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
+113
View File
@@ -0,0 +1,113 @@
/* =====================================================================
EIN ZIP- UND XLSX-SCHREIBER FUER PRUEFUNGEN (14.09.2026)
Er steht hier und nicht in einer der Pruefdateien, weil ihn seit
heute ZWEI brauchen: pruef-xlsx (der Leser) und pruef-backstage-import
(der Weg durch die Oberflaeche). Zwei Abschriften waeren zwei
Gelegenheiten, dass die eine ein Feld anders schreibt als die andere
-- und dann misst man zwei verschiedene Dinge und nennt beide "eine
Excel-Datei".
Gebaut wird nach der echten Backstage-Form: alles in der
Zeichenkettentabelle (`t="s"`), ein Blatt, keine Formate.
`crc32` gibt es in node:zlib seit Node 22. Fehlt es, sagen die
Pruefungen das -- der dritte Ausgang, kein stiller Aussetzer.
===================================================================== */
import { deflateRawSync, crc32 } from "node:zlib";
export const kannXlsxBauen = typeof crc32 === "function";
export function zipBauen(eintraege) {
const lokale = [];
const zentrale = [];
let stelle = 0;
for (const [name, inhalt] of eintraege) {
const roh = Buffer.from(inhalt, "utf8");
const gepackt = deflateRawSync(roh);
const pruef = crc32(roh) >>> 0;
const nameB = Buffer.from(name, "utf8");
const lokal = Buffer.alloc(30);
lokal.writeUInt32LE(0x04034b50, 0);
lokal.writeUInt16LE(20, 4); // Version
lokal.writeUInt16LE(0, 6); // Merker
lokal.writeUInt16LE(8, 8); // Deflate
lokal.writeUInt32LE(pruef, 14);
lokal.writeUInt32LE(gepackt.length, 18);
lokal.writeUInt32LE(roh.length, 22);
lokal.writeUInt16LE(nameB.length, 26);
lokale.push(lokal, nameB, gepackt);
const zentral = Buffer.alloc(46);
zentral.writeUInt32LE(0x02014b50, 0);
zentral.writeUInt16LE(20, 4);
zentral.writeUInt16LE(20, 6);
zentral.writeUInt16LE(8, 10);
zentral.writeUInt32LE(pruef, 16);
zentral.writeUInt32LE(gepackt.length, 20);
zentral.writeUInt32LE(roh.length, 24);
zentral.writeUInt16LE(nameB.length, 28);
zentral.writeUInt32LE(stelle, 42);
zentrale.push(zentral, nameB);
stelle += lokal.length + nameB.length + gepackt.length;
}
const koerper = Buffer.concat(lokale);
const verzeichnis = Buffer.concat(zentrale);
const ende = Buffer.alloc(22);
ende.writeUInt32LE(0x06054b50, 0);
ende.writeUInt16LE(eintraege.length, 8);
ende.writeUInt16LE(eintraege.length, 10);
ende.writeUInt32LE(verzeichnis.length, 12);
ende.writeUInt32LE(koerper.length, 16);
return Buffer.concat([koerper, verzeichnis, ende]);
}
export const RELS = `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
</Relationships>`;
/** Baut eine .xlsx aus Zeilen. Alles wandert in die
* Zeichenkettentabelle – genau wie Backstage es tut. */
export function xlsxBauen(zeilen, { blattName = "Sheet1", extra = "" } = {}) {
const texte = [];
const nummer = (w) => {
const i = texte.indexOf(w);
if (i >= 0) return i;
texte.push(w);
return texte.length - 1;
};
const buchstabe = (n) => {
let s = "";
n += 1;
while (n > 0) { const r = (n - 1) % 26; s = String.fromCharCode(65 + r) + s; n = (n - r - 1) / 26; }
return s;
};
const zeilenXml = zeilen.map((z, r) => {
const zellen = z.map((w, c) => {
if (w === null || w === undefined || w === "") return "";
return `<c r="${buchstabe(c)}${r + 1}" t="s"><v>${nummer(String(w))}</v></c>`;
}).join("");
return `<row r="${r + 1}">${zellen}</row>`;
}).join("");
const blatt = `<?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${zeilenXml}</sheetData></worksheet>`;
const strings = `<?xml version="1.0" encoding="UTF-8"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${texte.length}" uniqueCount="${texte.length}">`
+ texte.map((t) => `<si><t>${t.replace(/&/g, "&amp;").replace(/</g, "&lt;")}</t></si>`).join("")
+ "</sst>";
const mappe = `<?xml version="1.0" encoding="UTF-8"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"${extra}><sheets><sheet name="${blattName}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
return zipBauen([
["xl/workbook.xml", mappe],
["xl/_rels/workbook.xml.rels", RELS],
["xl/sharedStrings.xml", strings],
["xl/worksheets/sheet1.xml", blatt],
]);
}
+100 -34
View File
@@ -34,6 +34,7 @@ import { notbremse } from "./helfer-notbremse.mjs";
notbremse(240_000, "pruef-backstage-import");
const HIER = dirname(fileURLToPath(import.meta.url));
const { xlsxBauen } = await import("./helfer-xlsx-bauen.mjs");
const PORT = 4389;
const BASIS = `http://127.0.0.1:${PORT}`;
const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-"));
@@ -455,6 +456,63 @@ console.log("\n=== Die Seite ===");
{ path: join(HIER, "pruef-netz-leer.png") });
}
/* ---- DIE BACKSTAGE-AUSGABE ALS DATEI (14.09.2026) ---------------
Filipes echte Ausgabe ("Creator:innendaten …xlsx") enthaelt ALLE
Creator auf einmal -- sie gehoert hierher und nicht in den
Einzel-Import. Nachgebaut wird ihre Form, inklusive der Titelzeile,
die "Creator:innen verwalten" ueber die Tabelle setzt. */
const netzDatei = xlsxBauen([
["Exportiert am :2026-09-11 13:10:35"],
["Datenzeitraum", "Creator*innen-Anmeldename", "Diamanten", "LIVE-Dauer"],
[`${gestern} ~ ${gestern}`, "nova_live", "12.480", "3Std. 4Min."],
[`${gestern} ~ ${gestern}`, "Kiro", "3.900", "1Std. 35Min."],
]);
await seite.setInputFiles("#netz-datei", {
name: "Creator_innendaten.xlsx",
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: netzDatei,
});
await seite.waitForTimeout(1800);
const ausDatei = await seite.evaluate(() => ({
feld: document.getElementById("netz-text")?.value || "",
fehler: document.getElementById("netz-fehler")?.textContent || "",
vorschau: document.getElementById("netz-vorschau")?.hidden,
text: document.getElementById("netz-vorschau")?.textContent || "",
}));
ok(ausDatei.fehler === "", `die Datei wird angenommen ("${ausDatei.fehler.slice(0, 40)}")`);
ok(ausDatei.feld.includes("nova_live") && ausDatei.feld.includes("Datenzeitraum"),
"das Einfuegefeld ist danach gefuellt -- ab da derselbe Weg wie beim Einfuegen");
ok(!ausDatei.feld.startsWith("Exportiert am"),
"die Titelzeile ist weg, die Ueberschrift steht oben");
ok(ausDatei.vorschau === false && /zugeordnet/.test(ausDatei.text),
`und die Vorschau erscheint von selbst ("${ausDatei.text.slice(0, 45)}…")`);
/* DER TEUERSTE FALL, gemessen statt vermutet: Ein ZEITRAUM ueber
mehrere Tage darf NICHT auf einen Tag geschrieben werden. Genau das
steht in Filipes echter Datei (2026-09-01 ~ 2026-09-11), und genau
das haette `datumLesen` stillschweigend auf den 1. September
gebucht -- elf Tage Diamanten, plausibel und falsch. */
const zeitraumDatei = xlsxBauen([
["Datenzeitraum", "Creator*innen-Anmeldename", "Diamanten"],
["2026-09-01 ~ 2026-09-11", "nova_live", "53.679"],
]);
await seite.setInputFiles("#netz-datei", {
name: "zeitraum.xlsx",
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: zeitraumDatei,
});
await seite.waitForTimeout(1800);
const spanne = await seite.evaluate(() =>
document.getElementById("netz-vorschau")?.textContent || "");
ok(spanne.includes("mehrere Tage") && spanne.includes("2026-09-01"),
`ein Zeitraum ueber mehrere Tage wird abgelehnt und BEGRUENDET ("${spanne.slice(0, 90)}…")`);
ok(!/1 von 1 Zeilen zugeordnet/.test(spanne),
"und die Zeile wird NICHT als zugeordnet gezaehlt");
if (process.env.SCHIRM) {
await seite.locator("#netz-dialog").screenshot(
{ path: join(HIER, "pruef-netz-zeitraum.png") });
}
await seite.click("#netz-zu");
await seite.waitForTimeout(300);
/* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der
@@ -466,61 +524,69 @@ console.log("\n=== Die Seite ===");
ok(/Backstage/.test(netzSatz),
"unter den Knöpfen steht dauerhaft, woher die Zahlen kommen");
/* ---- EINE EXCEL-DATEI IST EINE SACKGASSE MIT WEGWEISER ----------
Filipe: "meine excel datei kann ich nicht auswaehlen wieso??"
/* ---- EINE EXCEL-DATEI WIRD GELESEN (14.09.2026) -----------------
Filipe: "mach doch bitte so dass man alle dateien hoch laden könne
auch excel dateien. gib gas und krieg das hin."
Sie liess sich nicht einmal AUSWAEHLEN -- ausgegraut, ohne ein
Wort dazu. Jetzt laesst sie sich waehlen und bekommt eine Antwort,
die weiterhilft.
HIER STAND DAS GEGENTEIL, und es war richtig: Am 11.09. bekam eine
.xlsx eine Absage mit Wegweiser ("nimm den Knopf daneben"). Seit
heute wird sie gelesen -- die Pruefung wurde deshalb GEDREHT, nicht
geloescht.
GEPRUEFT WIRD AN EINEM ECHTEN ZIP-KOPF ("PK"), denn genau das ist
eine .xlsx. Die Erkennung haengt am INHALT, nicht an der Endung --
deshalb heisst die Testdatei hier bewusst ".xlsx" UND traegt den
richtigen Kopf; eine Pruefung ueber die Endung waere gruen, ohne
die Erkennung je zu beruehren. */
GEBAUT WIRD EINE ECHTE DATEI, kein Stueck Text mit ZIP-Anfang. Der
erste Entwurf schickte zehn Bytes "PK…" und war gruen, weil der
Server "ZIP-Ende fehlt" meldete -- das haette auch bei kaputtem
Leser gestimmt. */
const xlsxDatei = xlsxBauen([
["Datum", "Creator", "Diamanten", "LIVE-Dauer"],
[gestern, "@Nova_Live", "12.480", "3Std. 4Min."],
[gestern, "Kiro", "3.900", "1Std. 35Min."],
]);
await seite.click("#import-oeffnen");
await seite.waitForTimeout(300);
await seite.setInputFiles("#datei", {
name: "backstage.xlsx",
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00, 0x08, 0x00]),
buffer: xlsxDatei,
});
await seite.waitForTimeout(600);
await seite.waitForTimeout(1500);
const xlsx = await seite.evaluate(() => ({
text: document.getElementById("import-fehler")?.textContent || "",
fehler: document.getElementById("import-fehler")?.textContent || "",
vorschau: document.getElementById("vorschau")?.hidden,
los: document.getElementById("import-los")?.disabled,
feld: document.getElementById("datei")?.value || "",
text: document.getElementById("vorschau")?.textContent || "",
}));
ok(/Excel/.test(xlsx.text), `es steht da, dass es eine Excel-Datei ist (${xlsx.text.slice(0, 50)}…)`);
ok(/Backstage-Tabelle einfügen/.test(xlsx.text),
"und welcher Knopf stattdessen hilft");
ok(/CSV/.test(xlsx.text), "und wie man sie umspeichert");
ok(xlsx.vorschau === true && xlsx.los === true,
"es entsteht KEINE Vorschau und der Übernehmen-Knopf bleibt gesperrt");
ok(xlsx.feld === "",
"und das Feld ist wieder leer -- eine Datei, die nicht geht, bleibt nicht stehen");
if (process.env.SCHIRM) {
await seite.locator("#import-dialog").screenshot(
{ path: join(HIER, "pruef-import-excel.png") });
}
ok(xlsx.fehler === "",
`die Excel-Datei wird ohne Absage gelesen ("${xlsx.fehler.slice(0, 50)}")`);
ok(xlsx.vorschau === false, "und es entsteht eine Vorschau");
ok(xlsx.text.includes("Diamanten") && xlsx.text.includes("LIVE-Dauer"),
`die Vorschau nennt die erkannten Spalten ("${xlsx.text.slice(0, 55)}…")`);
/* GEGENPROBE: Eine echte Text-Tabelle geht durch denselben Weg
weiterhin hindurch. Sonst hiesse das oben nur, dass gar nichts
mehr eingelesen wird. */
/* DIE ALTE .xls IST ETWAS ANDERES -- kein ZIP, sondern BIFF. Sie
bekommt weiterhin eine Absage, und die muss WEITERHELFEN. */
await seite.setInputFiles("#datei", {
name: "alt.xls",
mimeType: "application/vnd.ms-excel",
buffer: Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, 0, 0, 0, 0]),
});
await seite.waitForTimeout(700);
const altText = await seite.evaluate(() =>
document.getElementById("import-fehler")?.textContent || "");
ok(altText.includes(".xls") && (altText.includes("xlsx") || altText.includes("CSV")),
`eine alte .xls bekommt einen Weg gezeigt ("${altText.slice(0, 55)}…")`);
/* GEGENPROBE: Eine echte Text-Tabelle geht weiterhin durch. */
await seite.setInputFiles("#datei", {
name: "tage.csv",
mimeType: "text/csv",
buffer: Buffer.from(`Datum;Diamanten;LIVE-Dauer (Minuten)
${gestern};1234;90
`, "utf8"),
buffer: Buffer.from("Datum;Diamanten;LIVE-Dauer (Minuten)" + String.fromCharCode(10)
+ gestern + ";1234;90" + String.fromCharCode(10), "utf8"),
});
await seite.waitForTimeout(900);
const csv = await seite.evaluate(() => ({
fehler: document.getElementById("import-fehler")?.textContent || "",
vorschau: document.getElementById("vorschau")?.hidden,
}));
ok(csv.vorschau === false && !/Excel/.test(csv.fehler),
ok(csv.vorschau === false && csv.fehler === "",
`eine CSV kommt weiterhin durch (Vorschau da: ${csv.vorschau === false})`);
await seite.click("#import-zu");
await seite.waitForTimeout(250);
+342
View File
@@ -0,0 +1,342 @@
/* EXCEL-DATEIEN LESEN (14.09.2026)
Filipe: "mach doch bitte so dass man alle dateien hoch laden könne
auch excel dateien. gib gas und krieg das hin."
---------------------------------------------------------------------
WOGEGEN HIER GEPRÜFT WIRD
Die Vorlage sind ZWEI ECHTE Backstage-Ausgaben (11. und 12.09.2026),
die auf dem Rechner lagen. Sie haben mir gesagt, was Backstage
wirklich schreibt – und das war an drei Stellen anders, als ich es
mir gedacht hätte:
* ALLE Zellen sind `t="s"` (Zeichenkettentabelle). Backstage legt
sogar Zahlen als Text ab.
* "Creator:innen verwalten" hat in Zeile 1 nur einen Titel
("Exportiert am :…"), die Überschriften stehen in Zeile 2.
* Die Spalte "LIVE-Dauer" enthält `86Std. 10Min. 52Sek.` und
"Datenzeitraum" einen ZEITRAUM: `2026-09-01 ~ 2026-09-11`.
DIE ECHTEN DATEIEN STEHEN NICHT IM REPO – in ihnen stehen die Namen
und Zahlen von Filipes Creator:innen. Stattdessen baut diese Prüfung
eine .xlsx mit **derselben Form** und erfundenen Namen. Die echten
Dateien haben die Form belegt; die Prüfung hält sie fest.
WARUM DIE DATEI HIER GEBAUT WIRD UND NICHT DANEBEN LIEGT: Eine
Beispieldatei im Repo altert unbemerkt. Eine, die im Lauf entsteht,
sagt in ihrem Quelltext, was sie darstellt – und wer sie ändert,
sieht sofort, was er ändert.
Aufruf: node server/pruef-xlsx.mjs
===================================================================== */
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { xlsxLesen, tabelleAbKopf, alsText, istZip, istAltesExcel, XlsxFehler } =
await import("./workspace-xlsx.js");
const { dauerLesen, zeitraumLesen, datumLesen, zahlLesen, csvZerlegen, spaltenRaten } =
await import("./workspace-leistung.js");
/* =======================================================================
EIN ZIP-SCHREIBER — damit die Prüfung ECHTE Dateien erzeugt
=======================================================================
Ohne ihn müsste ich eine fertige .xlsx ins Repo legen und hoffen,
dass sie die Wirklichkeit trifft. So steht jedes Byte hier im
Quelltext, und die Prüfung kann Fälle bauen, die es als Datei gar
nicht gibt (kaputtes Verzeichnis, fehlendes Blatt, leere Tabelle).
`crc32` gibt es in Node seit v22 – fällt es aus, sagt die Prüfung
das (dritter Ausgang) statt stillschweigend etwas Falsches zu bauen.
======================================================================= */
const { zipBauen, xlsxBauen, RELS, kannXlsxBauen } =
await import("./helfer-xlsx-bauen.mjs");
if (!kannXlsxBauen) {
console.error("KONNTE NICHT NACHSEHEN: node:zlib kennt kein crc32 "
+ "(gebraucht wird Node 22+). Die Pruefung baut ihre Dateien selbst "
+ "und kann ohne Pruefsumme keine gueltige .xlsx erzeugen.");
process.exit(2);
}
/* =======================================================================
1. Die Form, die Backstage wirklich liefert
======================================================================= */
console.log("\n=== Die Backstage-Form ===");
const BACKSTAGE = [
["Datenzeitraum", "Creator*in-ID", "Creator*innen-Anmeldename", "Gruppe",
"Diamanten", "LIVE-Dauer", "Gültige LIVE-Gehen-Tage", "Neue Follower*innen"],
["2026-09-06 ~ 2026-09-06", "700001", "nova_live", "Spicy",
"12.480", "86Std. 10Min. 52Sek.", "11", "205"],
["2026-09-06 ~ 2026-09-06", "700002", "kiro_on", "Spicy",
"3.900", "2Std. 5Min.", "4", "18"],
];
{
const datei = xlsxBauen(BACKSTAGE);
ok(istZip(datei), "die gebaute Datei ist ein ZIP (wie eine echte .xlsx)");
const { blatt, zeilen } = xlsxLesen(datei);
ok(blatt === "Sheet1", `das Blatt heisst "${blatt}"`);
ok(zeilen.length === 3, `drei Zeilen gelesen (${zeilen.length})`);
ok(zeilen[0].length === 8, `acht Spalten (${zeilen[0].length})`);
ok(zeilen[0][0] === "Datenzeitraum" && zeilen[0][6] === "Gültige LIVE-Gehen-Tage",
"die Umlaute kommen heil an");
ok(zeilen[1][5] === "86Std. 10Min. 52Sek.",
`die LIVE-Dauer steht woertlich da ("${zeilen[1][5]}")`);
}
/* =======================================================================
2. Die Titelzeile, über die ich gestolpert wäre
======================================================================= */
console.log("\n=== Eine Titelzeile über der Tabelle ===");
{
/* So sieht "Creator:innen verwalten" aus: Zeile 1 eine einzige
Zelle, die Überschriften erst darunter. */
const mitTitel = [["Exportiert am :2026-09-11 13:10:35"], ...BACKSTAGE];
const { zeilen } = xlsxLesen(xlsxBauen(mitTitel));
ok(zeilen.length === 4, `vier Zeilen in der Datei (${zeilen.length})`);
const t = tabelleAbKopf(zeilen);
ok(t.uebersprungen === 1, `eine Titelzeile faellt weg (${t.uebersprungen})`);
ok(t.zeilen[0][0] === "Datenzeitraum",
`die Ueberschrift ist jetzt die richtige ("${t.zeilen[0][0]}")`);
/* GEGENPROBE: Ohne Titelzeile darf NICHTS wegfallen -- sonst waere
die Regel eine, die immer eine Zeile frisst. */
const ohne = tabelleAbKopf(xlsxLesen(xlsxBauen(BACKSTAGE)).zeilen);
ok(ohne.uebersprungen === 0, `ohne Titel faellt nichts weg (${ohne.uebersprungen})`);
/* UND EINE ECHTE SCHMALE TABELLE bleibt ganz. Drei Spalten sind eine
Tabelle, keine Ueberschrift. */
const schmal = tabelleAbKopf(
xlsxLesen(xlsxBauen([["Datum", "Diamanten", "Dauer"], ["2026-09-06", "10", "20"]])).zeilen);
ok(schmal.uebersprungen === 0, `eine schmale Tabelle bleibt unangetastet (${schmal.uebersprungen})`);
}
/* =======================================================================
3. Der teuerste Fehler: ein Zeitraum, der wie ein Tag aussieht
======================================================================= */
console.log("\n=== Zeitraum ist kein Tag ===");
{
ok(datumLesen("2026-09-01 ~ 2026-09-11") === "2026-09-01",
"datumLesen allein greift sich weiterhin den ersten Tag – deshalb braucht es zeitraumLesen");
const z = zeitraumLesen("2026-09-01 ~ 2026-09-11");
ok(z?.von === "2026-09-01" && z?.bis === "2026-09-11",
`der Zeitraum wird als solcher erkannt (${z?.von} bis ${z?.bis})`);
const einTag = zeitraumLesen("2026-09-06 ~ 2026-09-06");
ok(einTag?.von === einTag?.bis,
"ein Zeitraum von EINEM Tag ist kein Zeitraum -- der geht durch");
ok(zeitraumLesen("2026-09-06") === null,
"ein einzelnes Datum faellt nicht darauf herein");
ok(zeitraumLesen("Diamanten") === null, "und Text erst recht nicht");
const deutsch = zeitraumLesen("01.09.2026 - 11.09.2026");
ok(deutsch?.von === "2026-09-01" && deutsch?.bis === "2026-09-11",
"auch die deutsche Schreibweise");
}
/* =======================================================================
4. Die Einheit ist die Falle
======================================================================= */
console.log("\n=== Dauer mit Einheit ===");
{
const faelle = [
["86Std. 10Min. 52Sek.", 5171, "die echte Backstage-Schreibweise"],
["86 Std 10 Min", 5170, "mit Leerzeichen"],
["2 Stunden 5 Minuten", 125, "ausgeschrieben"],
["3h 20m", 200, "englisch kurz"],
["86:10:52", 5171, "als Uhrzeit"],
["184", 184, "blosse Zahl bleibt eine Zahl (Minuten, wie bisher)"],
["0", 0, "Null ist ein Wert, nicht nichts"],
];
for (const [text, soll, warum] of faelle) {
const ist = dauerLesen(text);
ok(ist === soll, `"${text}" -> ${ist} (erwartet ${soll}) – ${warum}`);
}
ok(dauerLesen("") === null && dauerLesen(null) === null,
"leer bleibt leer -- und wird nicht zu 0");
/* WARUM DAS EINE EIGENE FUNKTION BRAUCHTE: Der alte Weg ergab hier
nichts. Haette er "86" geliefert, stuenden 86 Minuten statt 5171
in der Datenbank -- plausibel und um Faktor 60 daneben. */
ok(zahlLesen("86Std. 10Min. 52Sek.") === null,
"zahlLesen allein kommt damit nicht klar -- das ist der Grund fuer dauerLesen");
}
/* =======================================================================
5. Der ganze Weg: Datei -> Text -> Zerlegung -> Spalten
======================================================================= */
console.log("\n=== Vom Excel bis zur Spaltenzuordnung ===");
{
const { zeilen } = xlsxLesen(xlsxBauen([["Exportiert am :…"], ...BACKSTAGE]));
const text = alsText(tabelleAbKopf(zeilen).zeilen);
ok(text.split("\n").length === 3, `drei Textzeilen (${text.split("\n").length})`);
ok(text.includes("\t"), "TAB-getrennt -- genau wie eine eingefuegte Tabelle");
const tabelle = csvZerlegen(text);
ok(tabelle.length === 3, `csvZerlegen findet drei Zeilen (${tabelle.length})`);
ok(tabelle[0].length === 8, `und acht Spalten (${tabelle[0].length})`);
const zu = spaltenRaten(tabelle[0]);
ok(tabelle[0][zu.diamanten] === "Diamanten", "die Diamanten-Spalte wird gefunden");
ok(tabelle[0][zu.dauer_min] === "LIVE-Dauer", "die Dauer-Spalte ebenfalls");
ok(tabelle[0][zu.follower_neu] === "Neue Follower*innen", "und die Follower-Spalte");
ok(tabelle[0][zu.tag] === "Datenzeitraum", "und der Zeitraum als Datumsspalte");
/* GEFUNDEN AM ECHTEN EXPORT (14.09.2026): Das alte Muster
`/datum|date|tag|day/i` erklaerte "Tage seit dem Beitritt" zur
Datumsspalte -- Wert "65". In der Leistungstabelle daneben traefe
es "Gueltige LIVE-Gehen-Tage" genauso. Beide sind Zaehlungen, kein
Datum.
Die Gegenprobe steht daneben, damit "trifft nicht" nicht einfach
heisst, dass gar nichts mehr trifft. */
for (const [name, sollTreffen] of [
["Datenzeitraum", true], ["Datum", true], ["Tag", true], ["Day", true],
["Tage seit dem Beitritt", false], ["Gültige LIVE-Gehen-Tage", false],
["Livestreams", false],
]) {
const trifft = spaltenRaten([name]).tag === 0;
ok(trifft === sollTreffen,
`"${name}" ${sollTreffen ? "ist" : "ist NICHT"} die Datumsspalte (erkannt: ${trifft})`);
}
/* Und die Werte, die dahinter herauskommen. */
ok(zahlLesen(tabelle[1][zu.diamanten]) === 12480,
`"12.480" wird zu ${zahlLesen(tabelle[1][zu.diamanten])} und nicht zu 12,48`);
ok(dauerLesen(tabelle[1][zu.dauer_min]) === 5171,
`und die Dauer zu ${dauerLesen(tabelle[1][zu.dauer_min])} Minuten`);
}
/* =======================================================================
6. Lücken, Zahlen, Datumsformate
======================================================================= */
console.log("\n=== Was eine Tabelle sonst noch enthaelt ===");
{
/* FEHLENDE ZELLEN VERSCHIEBEN NICHTS. In der Datei steht eine leere
Zelle gar nicht -- wer stur der Reihe nach liest, schiebt ab dort
jede Spalte um eins. Die Zeile sieht danach voll aus. */
const loecher = [["A", "B", "C", "D"], ["1", "", "", "4"]];
const { zeilen } = xlsxLesen(xlsxBauen(loecher));
ok(zeilen[1].length === 4, `die Luecken-Zeile hat vier Felder (${zeilen[1].length})`);
ok(zeilen[1][0] === "1" && zeilen[1][3] === "4",
`und "4" steht in Spalte 4, nicht in Spalte 2 ("${zeilen[1][3]}")`);
}
{
/* ECHTE ZAHLEN UND ECHTE DATEN -- also nicht die Backstage-Form,
sondern das, was entsteht, sobald jemand die Datei in Excel
oeffnet und speichert. Dann stehen dort Seriennummern. */
const blatt = `<?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>
<row r="1"><c r="A1" t="inlineStr"><is><t>Datum</t></is></c><c r="B1" t="inlineStr"><is><t>Diamanten</t></is></c></row>
<row r="2"><c r="A2" s="1"><v>46271</v></c><c r="B2"><v>12480</v></c></row>
<row r="3"><c r="A3" s="0"><v>46271</v></c><c r="B3" t="b"><v>1</v></c></row>
</sheetData></worksheet>`;
const styles = `<?xml version="1.0" encoding="UTF-8"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<cellXfs count="2"><xf numFmtId="0"/><xf numFmtId="14"/></cellXfs></styleSheet>`;
const mappe = `<?xml version="1.0" encoding="UTF-8"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Zahlen" sheetId="1" r:id="rId1"/></sheets></workbook>`;
const datei = zipBauen([
["xl/workbook.xml", mappe],
["xl/_rels/workbook.xml.rels", RELS],
["xl/styles.xml", styles],
["xl/worksheets/sheet1.xml", blatt],
]);
const { blatt: name, zeilen } = xlsxLesen(datei);
ok(name === "Zahlen", `der Blattname kommt aus der Mappe ("${name}")`);
ok(zeilen[0][0] === "Datum", "eingebettete Zeichenketten (inlineStr) werden gelesen");
ok(zeilen[1][0] === "2026-09-06",
`die Seriennummer 46271 wird zum Datum ("${zeilen[1][0]}")`);
ok(zeilen[1][1] === "12480", `eine gewoehnliche Zahl bleibt eine Zahl ("${zeilen[1][1]}")`);
/* GEGENPROBE ZUM DATUM: DIESELBE Zahl ohne Datumsformat bleibt eine
Zahl. Ohne sie hiesse die Zeile darueber nur, dass alles zu einem
Datum wird. */
ok(zeilen[2][0] === "46271",
`dieselbe Zahl ohne Datumsformat bleibt eine Zahl ("${zeilen[2][0]}")`);
ok(zeilen[2][1] === "WAHR", `ein Wahrheitswert wird lesbar ("${zeilen[2][1]}")`);
}
/* =======================================================================
7. Was schiefgehen kann, muss SAGEN was schiefging
======================================================================= */
console.log("\n=== Der dritte Ausgang ===");
{
const versuch = (puffer, was) => {
try { xlsxLesen(puffer); return { ok: true }; }
catch (f) { return { ok: false, name: f?.name, text: f?.message }; }
};
const alt = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, 0, 0]);
ok(istAltesExcel(alt), "eine alte .xls wird am Inhalt erkannt");
const a = versuch(alt);
ok(!a.ok && a.name === "XlsxFehler" && /\.xlsx|CSV/.test(a.text),
`und bekommt eine Antwort, die weiterhilft ("${(a.text || "").slice(0, 46)}…")`);
const b = versuch(Buffer.from("Datum;Diamanten\n2026-09-06;10\n", "utf8"));
ok(!b.ok && /keine Excel-Datei/i.test(b.text || ""),
`eine CSV wird nicht fuer eine Excel-Datei gehalten ("${(b.text || "").slice(0, 40)}…")`);
/* Ein ZIP, in dem kein Tabellenblatt steckt. */
const c = versuch(zipBauen([["xl/workbook.xml", "<workbook/>"]]));
ok(!c.ok && /Tabellenblatt/.test(c.text || ""),
`ein ZIP ohne Blatt sagt genau das ("${(c.text || "").slice(0, 40)}…")`);
/* Ein abgeschnittenes ZIP -- die haeufigste kaputte Datei ueberhaupt. */
const heil = xlsxBauen(BACKSTAGE);
const d = versuch(heil.subarray(0, Math.floor(heil.length / 2)));
ok(!d.ok && d.name === "XlsxFehler",
`eine abgeschnittene Datei wird abgewiesen ("${(d.text || "").slice(0, 40)}…")`);
/* GEGENPROBE: Die heile Datei geht durch. Ohne sie hiesse alles
oben nur, dass gar nichts gelesen wird. */
ok(versuch(heil).ok, "die heile Datei dagegen wird gelesen");
}
/* =======================================================================
8. Die Datumsrechnung, an bekannten Punkten festgenagelt
=======================================================================
Beim Schreiben dieser Datei habe ich mich selbst vertan: Ich hielt
46264 fuer den 06.09.2026, es ist der 30.08. Der Code hatte recht,
meine Erwartung war falsch.
Damit vor allem der NULLPUNKT nicht unbemerkt verrutscht, stehen hier
zwei Werte, die jeder nachschlagen kann. Verrutscht die Basis um
einen Tag, faellt es hier auf und nicht erst in einer Tabelle.
======================================================================= */
console.log("");
console.log("=== Der Nullpunkt der Excel-Zeitrechnung ===");
{
const alsDatum = (n) => {
const blatt = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
+ '<sheetData><row r="1"><c r="A1" s="1"><v>' + n + '</v></c></row></sheetData></worksheet>';
const styles = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
+ '<cellXfs count="2"><xf numFmtId="0"/><xf numFmtId="14"/></cellXfs></styleSheet>';
const mappe = '<?xml version="1.0"?>'
+ '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
+ 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
+ '<sheets><sheet name="X" sheetId="1" r:id="rId1"/></sheets></workbook>';
return xlsxLesen(zipBauen([
["xl/workbook.xml", mappe],
["xl/_rels/workbook.xml.rels", RELS],
["xl/styles.xml", styles],
["xl/worksheets/sheet1.xml", blatt],
])).zeilen[0][0];
};
for (const [n, soll] of [[36526, "2000-01-01"], [45292, "2024-01-01"]]) {
const ist = alsDatum(n);
ok(ist === soll, `Seriennummer ${n} ist der ${soll} (gelesen: ${ist})`);
}
/* DIE EINE STELLE, AN DER ES NICHT STIMMT -- und sie steht hier,
damit niemand sie fuer einen Fehler haelt: Excel haelt 1900
faelschlich fuer ein Schaltjahr. Wer den Nullpunkt so legt, dass
alle Daten ab dem 01.03.1900 stimmen, liegt davor um einen Tag
daneben. In einer Leistungstabelle kommt kein Datum von 1900 vor. */
ok(alsDatum(1) === "1899-12-31",
"vor dem 01.03.1900 liegt die Rechnung um einen Tag daneben (Excels Schaltjahr-Irrtum)");
}
console.log(`\n${geprueft} Pruefungen`);
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
process.exit(fehler ? 1 : 0);
+198 -3
View File
@@ -43,6 +43,7 @@
===================================================================== */
import express from "express";
import { xlsxLesen, tabelleAbKopf, alsText } from "./workspace-xlsx.js";
import {
db, protokolliere, echteIp, sitzungLesen, istLeitung,
sichtbareCreatorIds, ROLLEN_SORTIERUNG, heuteLokal,
@@ -569,6 +570,102 @@ export function zahlLesen(roh) {
return Number.isFinite(n) ? n : null;
}
/** Macht aus "86Std. 10Min. 52Sek." Minuten.
*
* GEMESSEN AN DER ECHTEN BACKSTAGE-AUSGABE (14.09.2026): Die Spalte
* "LIVE-Dauer" enthaelt dort woertlich `86Std. 10Min. 52Sek.` -- ohne
* Leerzeichen vor der Einheit. `zahlLesen` machte daraus `null`, weil
* "86.10.52." keine Zahl ist. Die Dauer fiel also stillschweigend weg;
* der Tag wurde mit Diamanten, aber ohne Minuten geschrieben.
*
* DIE EINHEIT IST DIE FALLE, nicht die Zahl. Haette `zahlLesen` hier
* "86" zurueckgegeben -- was bei einem anderen Trennzeichen leicht
* passiert waere --, stuenden 86 Minuten statt 5171 in der Datenbank.
* Das sieht richtig aus und ist um Faktor 60 daneben.
*
* Verstanden werden:
* "86Std. 10Min. 52Sek." deutsche Backstage-Ausgabe
* "86 Std 10 Min" mit Leerzeichen
* "3h 20m", "3 hrs 20 min" englische Fassungen
* "86:10:52" / "3:20" Doppelpunktform
* "184" blosse Zahl -> Minuten, wie bisher
*
* OHNE EINHEIT BLEIBT ES EINE ZAHL. Eine blosse "184" als Stunden zu
* deuten waere geraten; die bestehenden Exporte liefern dort Minuten,
* und dabei bleibt es.
*/
export function dauerLesen(roh) {
const t = String(roh ?? "").trim();
if (!t) return null;
/* Doppelpunktform zuerst: "86:10:52" ist eindeutig und wuerde von der
Einheitensuche unten nicht erkannt. Zwei Teile heissen Stunden und
Minuten -- so schreibt es jede Uhr. */
const uhr = /^(\d+):([0-5]?\d)(?::([0-5]?\d))?$/.exec(t);
if (uhr) {
const std = Number(uhr[1]);
const min = Number(uhr[2]);
const sek = uhr[3] === undefined ? 0 : Number(uhr[3]);
return Math.round(std * 60 + min + sek / 60);
}
/* Einheiten einsammeln. Der Punkt hinter "Std." gehoert nicht dazu,
die Reihenfolge im Text ist egal, und jede Einheit zaehlt nur
einmal -- "1 Std 2 Std" ist kein gueltiger Wert und soll nicht
stillschweigend zu drei Stunden werden. */
const einheiten = [
["std", /(\d+(?:[.,]\d+)?)\s*(?:std|stunden|stunde|hrs|hours|hour|h)(?![a-z])/i],
["min", /(\d+(?:[.,]\d+)?)\s*(?:min|minuten|minute|mins|m)(?![a-z])/i],
["sek", /(\d+(?:[.,]\d+)?)\s*(?:sek|sekunden|sekunde|secs|sec|s)(?![a-z])/i],
];
let minuten = 0;
let gefunden = false;
for (const [art, muster] of einheiten) {
const m = muster.exec(t);
if (!m) continue;
const n = Number(String(m[1]).replace(",", "."));
if (!Number.isFinite(n)) continue;
gefunden = true;
if (art === "std") minuten += n * 60;
else if (art === "min") minuten += n;
else minuten += n / 60;
}
if (gefunden) return Math.round(minuten);
/* Keine Einheit erkannt -> wie bisher eine blosse Zahl. */
return zahlLesen(t);
}
/** Ist das ein ZEITRAUM statt eines Tages?
*
* Backstage schreibt in "Datenzeitraum" woertlich
* `2026-09-01 ~ 2026-09-11`. `datumLesen` griff sich davon die erste
* Haelfte und gab "2026-09-01" zurueck -- elf Tage Diamanten waeren
* damit auf den 1. September geschrieben worden. Gemessen am echten
* Export vom 12.09.2026, nicht vermutet.
*
* DAS IST DER TEUERSTE FEHLER, den dieser Weg machen kann: Die Zahl
* steht da, sie ist gross, sie sieht richtig aus, und niemand kann
* spaeter sagen, dass sie elf Tage enthaelt.
*
* Rueckgabe: {von, bis} bei einem erkannten Zeitraum, sonst null.
* Ein Zeitraum von EINEM Tag (von === bis) ist kein Zeitraum -- dann
* ist die Datei genau das, was hier gebraucht wird.
*/
export function zeitraumLesen(roh) {
const t = String(roh ?? "").trim();
/* Zwei Datumsangaben, getrennt durch ~, -, bis oder to. Das Muster
verlangt ZWEI vollstaendige Daten; ein einzelnes "2026-09-06"
faellt nicht darauf herein. */
const m = /^(\d{4}-\d{2}-\d{2})[^\d]{1,6}(\d{4}-\d{2}-\d{2})$/.exec(t)
|| /^(\d{1,2}\.\d{1,2}\.\d{4})[^\d]{1,6}(\d{1,2}\.\d{1,2}\.\d{4})$/.exec(t);
if (!m) return null;
const von = datumLesen(m[1]);
const bis = datumLesen(m[2]);
if (!von || !bis) return null;
return { von, bis };
}
/** Macht aus einem Textwert ein Datum "JJJJ-MM-TT".
* Erkennt 2026-09-06, 06.09.2026 und 09/06/2026. */
export function datumLesen(roh) {
@@ -589,7 +686,21 @@ export function datumLesen(roh) {
* Nur ein Vorschlag -- bestätigt wird er in der Vorschau. */
export function spaltenRaten(kopf) {
const muster = {
tag: /datum|date|tag|day/i,
/* "tag" UND "day" NUR ALS GANZES WORT (14.09.2026).
Gemessen an der echten Backstage-Mitgliederliste: Dort steht
"Tage seit dem Beitritt", und das alte Muster erklaerte diese
Spalte zur Datumsspalte -- Wert "65". In der Leistungstabelle
daneben trifft es "Gueltige LIVE-Gehen-Tage" genauso.
Aufgefallen ist es nicht beim Lesen, sondern als ich den ganzen
Weg mit der echten Datei durchlaufen liess.
"zeitraum" kam dazu, weil Backstages Spalte "Datenzeitraum"
heisst -- sie wurde bisher nur zufaellig gefunden, weil in
"Daten" die englische Silbe "date" steckt. Auf Zufall soll das
nicht beruhen. */
tag: /datum|date|zeitraum|^\s*tage?\s*$|^\s*day\s*$/i,
diamanten: /diamant|diamond/i,
dauer_min: /dauer|duration|minut|zeit.*live|live.*zeit/i,
zuschauer_avg: /(durchschnitt|avg|average|⌀|ø).*(zuschauer|viewer)|(zuschauer|viewer).*(durchschnitt|avg)/i,
@@ -749,7 +860,28 @@ function netzwerkLesen(text, person, tagVorgabe) {
const rohWer = String(rohHandle).trim() || String(rohName).trim();
if (!rohWer) { schlecht.push({ zeile: i + 1, grund: "Keine Person in der Zeile." }); continue; }
const tag = ausTabelle ? datumLesen(z[zu.tag]) : tagVorgabe;
/* EIN ZEITRAUM IST KEIN TAG (14.09.2026).
Backstage schreibt in "Datenzeitraum" `2026-09-01 ~ 2026-09-11`.
`datumLesen` griff sich davon den ersten Tag -- elf Tage
Diamanten waeren auf den 1. September geschrieben worden. Die
Zahl steht da, sie ist gross, sie sieht richtig aus, und niemand
kann spaeter sagen, dass elf Tage darin stecken.
Gemessen am echten Export vom 12.09.2026, nicht vermutet.
Ein Zeitraum von EINEM Tag geht durch -- dann ist die Datei
genau das, was hier gebraucht wird. */
const zeitraum = ausTabelle ? zeitraumLesen(z[zu.tag]) : null;
if (zeitraum && zeitraum.von !== zeitraum.bis) {
schlecht.push({ zeile: i + 1,
grund: `Zeitraum ${zeitraum.von} bis ${zeitraum.bis} – das sind mehrere Tage `
+ "auf einmal. Stell in Backstage den Zeitraum auf EINEN Tag." });
continue;
}
const tag = ausTabelle
? (zeitraum ? zeitraum.von : datumLesen(z[zu.tag]))
: tagVorgabe;
if (!tag) { schlecht.push({ zeile: i + 1, grund: `Datum unlesbar: "${z[zu.tag]}"` }); continue; }
if (tag > heuteLokal()) { schlecht.push({ zeile: i + 1, grund: `${tag} liegt in der Zukunft` }); continue; }
@@ -767,7 +899,11 @@ function netzwerkLesen(text, person, tagVorgabe) {
const werte = { creator_id: p.id, name: p.name, tag };
let hatZahl = false;
for (const feld of messwerte) {
const w = zahlLesen(z[zu[feld]]);
/* Die Dauer hat eine EINHEIT, die anderen Felder nicht.
"86Std. 10Min. 52Sek." ergibt sonst nichts (zahlLesen scheitert
an "86.10.52.") -- und bei einem anderen Trennzeichen waere es
schlimmer: 86 statt 5171, Faktor 60 daneben und plausibel. */
const w = feld === "dauer_min" ? dauerLesen(z[zu[feld]]) : zahlLesen(z[zu[feld]]);
werte[feld] = w;
if (w !== null && w !== undefined) hatZahl = true;
}
@@ -868,6 +1004,65 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import", gleicheHerkunft,
}
});
/* =====================================================================
EXCEL-DATEIEN (14.09.2026)
Filipe: "mach doch bitte so dass man alle dateien hoch laden könne
auch excel dateien. gib gas und krieg das hin."
Die Datei kommt roh herein und geht als TEXT wieder hinaus -- genau
in der Form, die auch beim Einfuegen aus dem Browser entsteht
(TAB-getrennt). Danach laeuft alles durch dieselbe Zerlegung,
dieselbe Vorschau und dieselbe Bestaetigung wie bisher.
WARUM NICHT GLEICH IMPORTIEREN: Eine Datei, die direkt schreibt,
nimmt dem Menschen den Blick auf das, was passieren wird. Die
Vorschau ist der ganze Schutz dieses Wegs; sie zu umgehen waere
bequem und falsch.
WER DARF: wer auch eintragen darf. Gelesen wird nichts Fremdes --
die Antwort enthaelt nur, was in der hochgeladenen Datei stand.
===================================================================== */
leistungRouter.post("/workspace/api/leistung/tabelle-lesen",
gleicheHerkunft, express.raw({ type: "*/*", limit: "12mb" }), (req, res) => {
try {
if (!darfEintragen(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" });
if (!Buffer.isBuffer(req.body) || !req.body.length) {
return res.status(400).json({ fehler: "Keine Datei empfangen." });
}
let gelesen;
try {
gelesen = xlsxLesen(req.body);
} catch (fehler) {
/* DER DRITTE AUSGANG: nicht "ging nicht", sondern WAS nicht
ging. Sonst sucht der Mensch den Fehler bei sich. */
if (fehler?.name === "XlsxFehler") {
return res.status(415).json({ fehler: fehler.message });
}
throw fehler;
}
const t = tabelleAbKopf(gelesen.zeilen);
if (!t.zeilen.length) {
return res.status(422).json({ fehler: "In der Tabelle steht nichts." });
}
const text = alsText(t.zeilen);
res.json({
text,
blatt: gelesen.blatt,
zeilen: t.zeilen.length,
spalten: t.zeilen[0]?.length || 0,
/* Wie viele Zeilen oben weggefallen sind. Steht in der Antwort,
damit die Oberflaeche es SAGEN kann -- eine Tabelle, die
stillschweigend anders anfaengt als die Datei, ist genau die
Sorte Hilfsbereitschaft, die spaeter niemand nachvollzieht. */
uebersprungen: t.uebersprungen,
});
} catch (fehler) {
console.error("[leistung] Tabelle lesen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
DER WEG UEBER DIE TIKTOK-DATEI IST ENTFERNT (11.09.2026)
+367
View File
@@ -0,0 +1,367 @@
/* =====================================================================
EXCEL-DATEIEN LESEN — ohne fremdes Paket, mit Bordmitteln
(14.09.2026)
Filipe: "mach doch bitte so dass man alle dateien hoch laden könne
auch excel dateien. gib gas und krieg das hin."
---------------------------------------------------------------------
WARUM SELBST GEBAUT UND NICHT `npm i xlsx`
Eine .xlsx ist ein ZIP-Archiv mit XML darin. Node kann beides von
Haus aus: `zlib.inflateRawSync` packt aus, der Rest ist Textarbeit.
Ein Paket dafür wäre eine Abhängigkeit mehr, die gepflegt, geprüft
und aktualisiert werden will -- für eine Datei mit neun Zeilen.
GEMESSEN AN DEN ECHTEN DATEIEN, nicht an der Dokumentation: Zwei
Backstage-Ausgaben vom 11. und 12.09.2026 lagen vor. Was sie zeigen:
* 12 Einträge im ZIP, alle mit Verfahren 8 (Deflate).
* Ein einziges Blatt.
* ALLE Zellen sind `t="s"` -- Backstage legt sogar Zahlen als
Zeichenkette ab. Datums-Seriennummern kommen darin also gar
nicht vor. (Der Leser kann sie trotzdem, siehe unten: Sobald
jemand die Datei in Excel öffnet und speichert, sind sie da.)
* Bei "Creator:innen verwalten" steht in Zeile 1 nur ein Titel
("Exportiert am :…"), die Überschriften stehen in Zeile 2.
---------------------------------------------------------------------
WAS DIESER LESER NICHT TUT
Er formatiert nicht. Eine Zelle wird zu dem Text, der darin steht --
Formeln werden mit ihrem zuletzt berechneten Wert gelesen, nicht
ausgerechnet. Das ist Absicht: Wer eine Tabelle mit Formeln
hochlädt, will die Zahlen, die er selbst gesehen hat.
===================================================================== */
import { inflateRawSync } from "node:zlib";
/* Ein ZIP-Eintrag, wie ihn das Zentralverzeichnis beschreibt. Gelesen
wird IMMER über das Zentralverzeichnis am Dateiende und nie durch
Vorwärtslaufen über die lokalen Köpfe: Bei einem Archiv mit
Datendeskriptoren (Bit 3 im Merker) steht die gepackte Länge im
lokalen Kopf auf 0, und wer sich darauf verlässt, liest nichts. */
const ENDE = 0x06054b50; // "PK\x05\x06"
const ZENTRAL = 0x02014b50; // "PK\x01\x02"
/** Fehler mit Grund -- der DRITTE AUSGANG, nicht ein stilles `null`.
* Wer eine kaputte Datei hochlädt, soll erfahren, WAS daran kaputt
* ist; "ging nicht" schickt ihn sonst auf die Suche bei sich selbst. */
export class XlsxFehler extends Error {
constructor(text) { super(text); this.name = "XlsxFehler"; }
}
/** Ist das überhaupt eine ZIP-Datei? Am INHALT erkannt, nicht am Namen.
* Eine umbenannte Datei ist keine andere Datei. */
export function istZip(puffer) {
return puffer?.length >= 4 && puffer[0] === 0x50 && puffer[1] === 0x4b
&& (puffer[2] === 0x03 || puffer[2] === 0x05 || puffer[2] === 0x07);
}
/** Alte .xls (BIFF, vor 2007) -- ein ganz anderes Format, kein ZIP.
* Wird nicht gelesen, aber ERKANNT, damit die Meldung stimmt. */
export function istAltesExcel(puffer) {
return puffer?.length >= 8 && puffer[0] === 0xd0 && puffer[1] === 0xcf
&& puffer[2] === 0x11 && puffer[3] === 0xe0;
}
/** Das Archiv auspacken -- Name -> Inhalt (Buffer). */
function zipLesen(puffer) {
/* Das Ende-Kennzeichen steht hinten und kann bis zu 64 KB Kommentar
hinter sich haben. Deshalb rückwärts suchen, nicht raten. */
let ende = -1;
const frueheste = Math.max(0, puffer.length - 22 - 65535);
for (let i = puffer.length - 22; i >= frueheste; i--) {
if (puffer.readUInt32LE(i) === ENDE) { ende = i; break; }
}
if (ende < 0) throw new XlsxFehler("Das ist keine gültige Excel-Datei (ZIP-Ende fehlt).");
const anzahl = puffer.readUInt16LE(ende + 10);
let stelle = puffer.readUInt32LE(ende + 16);
if (anzahl === 0xffff || stelle === 0xffffffff) {
throw new XlsxFehler("Die Datei ist zu groß für dieses Format (ZIP64).");
}
const dateien = new Map();
for (let i = 0; i < anzahl; i++) {
if (stelle + 46 > puffer.length || puffer.readUInt32LE(stelle) !== ZENTRAL) {
throw new XlsxFehler("Das Inhaltsverzeichnis der Datei ist beschädigt.");
}
const verfahren = puffer.readUInt16LE(stelle + 10);
const gepackt = puffer.readUInt32LE(stelle + 20);
const nameLang = puffer.readUInt16LE(stelle + 28);
const extraLang = puffer.readUInt16LE(stelle + 30);
const kommLang = puffer.readUInt16LE(stelle + 32);
const lokal = puffer.readUInt32LE(stelle + 42);
const name = puffer.subarray(stelle + 46, stelle + 46 + nameLang).toString("utf8");
/* Die Längen im LOKALEN Kopf gelten, nicht die im Zentralverzeichnis:
Nur dort steht, wie lang Name und Extrafeld an DIESER Stelle
wirklich sind -- sie dürfen sich unterscheiden. */
const lNameLang = puffer.readUInt16LE(lokal + 26);
const lExtraLang = puffer.readUInt16LE(lokal + 28);
const von = lokal + 30 + lNameLang + lExtraLang;
const roh = puffer.subarray(von, von + gepackt);
/* Nur die XML-Teile werden gebraucht. Bilder (Backstage legt ein
19-KB-Logo bei) bleiben ungepackt liegen -- das spart bei jeder
Datei die Arbeit und kann nicht schiefgehen. */
if (name.endsWith(".xml") || name.endsWith(".rels")) {
try {
dateien.set(name, verfahren === 0 ? roh : inflateRawSync(roh));
} catch {
throw new XlsxFehler(`Ein Teil der Datei ließ sich nicht entpacken (${name}).`);
}
}
stelle += 46 + nameLang + extraLang + kommLang;
}
return dateien;
}
/* ---------- XML, so viel wie nötig ------------------------------------ */
const ENTITAETEN = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
/** Entschärfte Zeichen zurückholen. `&#228;` und `&#xE4;` kommen in
* Exporten mit Umlauten wirklich vor -- ohne sie stünde in der
* Vorschau "Gltige LIVE-Gehen-Tage". */
function entschluesseln(text) {
if (!text.includes("&")) return text;
return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (ganz, inhalt) => {
if (inhalt[0] === "#") {
const n = inhalt[1] === "x" || inhalt[1] === "X"
? parseInt(inhalt.slice(2), 16) : parseInt(inhalt.slice(1), 10);
return Number.isFinite(n) ? String.fromCodePoint(n) : ganz;
}
return ENTITAETEN[inhalt] ?? ganz;
});
}
/** Die Zeichenkettentabelle. Ein Eintrag kann aus mehreren Stücken
* bestehen (`<r>`-Läufe, wenn Teile anders formatiert sind) -- die
* gehören zusammengesetzt, sonst fehlt die Hälfte des Namens. */
function zeichenkettenLesen(dateien) {
const roh = dateien.get("xl/sharedStrings.xml");
if (!roh) return [];
const text = roh.toString("utf8");
const liste = [];
for (const eintrag of text.matchAll(/<si(?:\s[^>]*)?>([\s\S]*?)<\/si>/g)) {
let stueck = "";
for (const t of eintrag[1].matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)) {
stueck += entschluesseln(t[1]);
}
liste.push(stueck);
}
return liste;
}
/* ---------- Datum: nur, wenn das Format es sagt ----------------------- */
/* Die eingebauten Zahlenformate, die ein Datum oder eine Uhrzeit sind.
Sie stehen in der Norm (ECMA-376) und nicht in der Datei -- deshalb
hier, und deshalb als Liste und nicht als Bereich: 23 bis 44 sind
KEINE Datumsformate, ein `>= 14 && <= 47` wäre falsch. */
const DATUMS_IDS = new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]);
/** Welche Zellvorlage zeigt ein Datum? Ergebnis: Menge von Vorlagen-
* nummern (`s`-Attribut der Zelle).
*
* WARUM DAS SEIN MUSS: In der Datei steht bei einem Datum die Zahl
* 46264. Ohne diese Zuordnung landet genau das in der Vorschau -- eine
* Zahl, die aussieht wie eine Zahl und ein Datum ist. */
function datumsVorlagen(dateien) {
const roh = dateien.get("xl/styles.xml");
const vorlagen = new Set();
if (!roh) return vorlagen;
const text = roh.toString("utf8");
/* Eigene Formate: Ein Format ist ein Datum, wenn darin d/m/y/h/s
außerhalb von Anführungszeichen vorkommen. Die Anführungszeichen
müssen weg, sonst macht `"Stand:"` aus jedem Format ein Datum. */
const eigene = new Set();
for (const m of text.matchAll(/<numFmt[^>]*numFmtId="(\d+)"[^>]*formatCode="([^"]*)"/g)) {
const code = entschluesseln(m[2]).replace(/"[^"]*"/g, "").replace(/\\./g, "");
if (/[dmyhs]/i.test(code)) eigene.add(Number(m[1]));
}
const block = /<cellXfs[^>]*>([\s\S]*?)<\/cellXfs>/.exec(text);
if (!block) return vorlagen;
let nummer = 0;
for (const xf of block[1].matchAll(/<xf\b[^>]*>/g)) {
const id = Number(/numFmtId="(\d+)"/.exec(xf[0])?.[1] ?? 0);
if (DATUMS_IDS.has(id) || eigene.has(id)) vorlagen.add(nummer);
nummer++;
}
return vorlagen;
}
/** Excel-Seriennummer -> "JJJJ-MM-TT" (oder mit Uhrzeit).
*
* DER SPRUNGTAG, DEN ES NIE GAB: Excel hält 1900 für ein Schaltjahr.
* Deshalb ist der Nullpunkt der 30.12.1899 und nicht der 31.12. --
* damit stimmen alle Tage ab dem 01.03.1900, und das sind alle, die
* hier je vorkommen. */
function ausSeriennummer(zahl, seit1904) {
const basis = seit1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
const ms = basis + Math.round(zahl * 86400 * 1000);
const d = new Date(ms);
if (!Number.isFinite(d.getTime())) return String(zahl);
const p = (n) => String(n).padStart(2, "0");
const datum = `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
const rest = zahl - Math.floor(zahl);
if (rest < 1 / 86400) return datum;
return `${datum} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
}
/* ---------- Spaltenname -> Nummer ------------------------------------- */
/** "A" -> 0, "Z" -> 25, "AA" -> 26.
* Gebraucht, weil leere Zellen in der Datei FEHLEN: Eine Zeile kann
* A, B, dann D enthalten. Wer stur der Reihe nach einliest, verschiebt
* ab dort jede Spalte -- und merkt es nicht, weil die Zeile voll
* aussieht. */
function spalteAus(bezug) {
const m = /^([A-Z]+)/.exec(bezug || "");
if (!m) return -1;
let n = 0;
for (const z of m[1]) n = n * 26 + (z.charCodeAt(0) - 64);
return n - 1;
}
/* ---------- Das Blatt ------------------------------------------------- */
/** Welches Blatt ist das erste? Der Name kommt aus workbook.xml, der
* Dateiname aus den Beziehungen. Fällt eines davon aus, wird auf
* sheet1.xml zurückgefallen -- mit Vermerk, nicht stillschweigend. */
function erstesBlatt(dateien) {
const mappe = dateien.get("xl/workbook.xml")?.toString("utf8") || "";
const bez = dateien.get("xl/_rels/workbook.xml.rels")?.toString("utf8") || "";
const eintrag = /<sheet\b[^>]*>/.exec(mappe)?.[0] || "";
const name = entschluesseln(/name="([^"]*)"/.exec(eintrag)?.[1] || "Tabelle1");
const rid = /r:id="([^"]*)"/.exec(eintrag)?.[1];
let pfad = null;
if (rid) {
const ziel = new RegExp(`<Relationship[^>]*Id="${rid}"[^>]*Target="([^"]*)"`).exec(bez)?.[1];
if (ziel) pfad = ziel.startsWith("/") ? ziel.slice(1) : `xl/${ziel.replace(/^\.\//, "")}`;
}
if (!pfad || !dateien.has(pfad)) pfad = "xl/worksheets/sheet1.xml";
return { name, pfad };
}
/**
* Eine .xlsx in Zeilen aus Text verwandeln.
*
* @param {Buffer} puffer die Datei
* @returns {{ blatt: string, zeilen: string[][] }}
*/
export function xlsxLesen(puffer) {
if (istAltesExcel(puffer)) {
throw new XlsxFehler(
"Das ist eine alte Excel-Datei (.xls). Speichere sie in Excel einmal "
+ "als .xlsx oder als CSV – dann geht sie durch.");
}
if (!istZip(puffer)) throw new XlsxFehler("Das ist keine Excel-Datei.");
const dateien = zipLesen(puffer);
const { name, pfad } = erstesBlatt(dateien);
const roh = dateien.get(pfad);
if (!roh) throw new XlsxFehler("In der Datei ist kein Tabellenblatt zu finden.");
const texte = zeichenkettenLesen(dateien);
const datumsZellen = datumsVorlagen(dateien);
const seit1904 = /date1904="(1|true)"/.test(
dateien.get("xl/workbook.xml")?.toString("utf8") || "");
const blatt = roh.toString("utf8");
const zeilen = [];
for (const zeile of blatt.matchAll(/<row\b[^>]*>([\s\S]*?)<\/row>/g)) {
const felder = [];
for (const zelle of zeile[1].matchAll(/<c\b([^>]*)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
const kopf = zelle[1];
const inhalt = zelle[2] || "";
const spalte = spalteAus(/r="([A-Z]+\d+)"/.exec(kopf)?.[1]);
const typ = /t="([^"]*)"/.exec(kopf)?.[1] || "n";
const vorlage = Number(/s="(\d+)"/.exec(kopf)?.[1] ?? -1);
let wert = "";
if (typ === "s") {
const i = Number(/<v(?:\s[^>]*)?>([\s\S]*?)<\/v>/.exec(inhalt)?.[1] ?? -1);
wert = texte[i] ?? "";
} else if (typ === "inlineStr") {
for (const t of inhalt.matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)) {
wert += entschluesseln(t[1]);
}
} else {
const roh2 = /<v(?:\s[^>]*)?>([\s\S]*?)<\/v>/.exec(inhalt)?.[1];
wert = roh2 === undefined ? "" : entschluesseln(roh2);
if (typ === "b") wert = wert === "1" ? "WAHR" : "FALSCH";
else if (typ === "e") wert = wert; // #DIV/0! usw. bleibt stehen
else if (typ === "n" && wert !== "" && datumsZellen.has(vorlage)
&& Number.isFinite(Number(wert))) {
wert = ausSeriennummer(Number(wert), seit1904);
}
}
/* An die RICHTIGE Stelle, nicht ans Ende. Fehlende Zellen werden
zu leeren Feldern -- siehe spalteAus(). */
if (spalte >= 0) {
while (felder.length < spalte) felder.push("");
felder[spalte] = wert;
} else {
felder.push(wert);
}
}
zeilen.push(felder);
}
return { blatt: name, zeilen };
}
/* =====================================================================
WO FÄNGT DIE TABELLE AN?
Backstages Ausgabe "Creator:innen verwalten" hat in Zeile 1 nur
"Exportiert am :2026-09-11 13:10:35" -- eine einzige Zelle. Die
Überschriften stehen in Zeile 2. Wer stur die erste Zeile als
Überschrift nimmt, bekommt eine Tabelle mit einer Spalte und meldet
"keine Diamanten-Spalte gefunden": eine richtige Meldung auf eine
falsche Fährte.
DIE REGEL MISST, SIE RÄT NICHT: Überschrift ist die erste Zeile, die
mindestens drei gefüllte Felder hat UND mindestens halb so breit ist
wie die breiteste Zeile der Datei. Eine Titelzeile ist damit sicher
draußen, eine echte schmale Tabelle (drei Spalten) sicher drin.
===================================================================== */
export function tabelleAbKopf(zeilen) {
const gefuellt = (z) => z.filter((x) => String(x ?? "").trim() !== "").length;
const breiteste = zeilen.reduce((a, z) => Math.max(a, gefuellt(z)), 0);
if (breiteste === 0) return { ab: 0, uebersprungen: 0, zeilen: [] };
let ab = 0;
for (let i = 0; i < zeilen.length; i++) {
const n = gefuellt(zeilen[i]);
if (n >= 3 && n * 2 >= breiteste) { ab = i; break; }
/* Nur führende Zeilen dürfen wegfallen. Findet sich gar keine
passende, bleibt es bei Zeile 0 -- lieber eine schlechte
Überschrift als eine verschluckte Tabelle. */
}
return { ab, uebersprungen: ab, zeilen: zeilen.slice(ab) };
}
/** Zeilen -> TAB-getrennter Text.
*
* WARUM TAB UND WARUM ÜBERHAUPT TEXT: Dahinter liegt `csvZerlegen`,
* seit dem 11.09.2026 auf Tabulatoren eingerichtet und mit 77
* Prüfungen abgesichert. Eine zweite Zerlegung für Excel wäre eine
* zweite Fassung derselben Regeln -- und die zweite ist die, in der
* ein Sonderfall fehlt. Die Excel-Datei wird also zu genau dem, was
* auch beim Einfügen aus dem Browser ankommt.
*
* Tabulatoren und Zeilenumbrüche IM FELD werden zu Leerzeichen. Sonst
* zerfiele ein Feld in zwei Spalten, und ab da stimmt keine Zeile
* mehr. */
export function alsText(zeilen) {
return zeilen
.map((z) => z.map((w) => String(w ?? "").replace(/[\t\r\n]+/g, " ")).join("\t"))
.join("\n");
}
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609120025",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609141135",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609120025",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609141135",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609120025",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141135",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609120025",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141135",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+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
+99 -8
View File
@@ -476,27 +476,67 @@
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;
}
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. */
if (!importText.trim()) {
@@ -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. */
+12 -12
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -252,10 +252,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/aufgaben.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/aufgaben.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -147,10 +147,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/automation.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/automation.js?v=202609141135" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -181,14 +181,14 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/checkliste.js?v=202609120025" defer></script>
<script src="assets/js/ampel.js?v=202609120025" defer></script>
<script src="assets/js/schulung.js?v=202609120025" defer></script>
<script src="assets/js/vorlagen.js?v=202609120025" defer></script>
<script src="assets/js/bereich.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/checkliste.js?v=202609141135" defer></script>
<script src="assets/js/ampel.js?v=202609141135" defer></script>
<script src="assets/js/schulung.js?v=202609141135" defer></script>
<script src="assets/js/vorlagen.js?v=202609141135" defer></script>
<script src="assets/js/bereich.js?v=202609141135" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -89,11 +89,11 @@
</main>
<script src="assets/js/ki.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/calls.js?v=202609120025" defer></script>
<script src="assets/js/ki.js?v=202609141135" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/calls.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,26 +12,26 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -341,10 +341,10 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609120025"></script>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025"></script>
<script src="assets/js/glocke.js?v=202609120025"></script>
<script src="assets/js/chat.js?v=202609120025"></script>
<script src="assets/js/bereiche.js?v=202609141135"></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135"></script>
<script src="assets/js/glocke.js?v=202609141135"></script>
<script src="assets/js/chat.js?v=202609141135"></script>
</body>
</html>
+15 -15
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/content.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/content.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -179,12 +179,12 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/checkliste.js?v=202609120025" defer></script>
<script src="assets/js/vorlagen.js?v=202609120025" defer></script>
<script src="assets/js/content.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/checkliste.js?v=202609141135" defer></script>
<script src="assets/js/vorlagen.js?v=202609141135" defer></script>
<script src="assets/js/content.js?v=202609141135" defer></script>
</body>
</html>
+5 -5
View File
@@ -36,15 +36,15 @@
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
die eine Seite, auf der man das Umfaerben sieht. -->
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609141135" />
</head>
<body class="gate gate--crew">
@@ -319,6 +319,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609120025" defer></script>
<script src="assets/js/gate.js?v=202609141135" defer></script>
</body>
</html>
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609120025",
"src": "/assets/img/app-symbole/crew-192.png?v=202609141135",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609120025",
"src": "/assets/img/app-symbole/crew-512.png?v=202609141135",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609120025",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609141135",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609120025",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609141135",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -108,10 +108,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/dateien.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/dateien.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -94,9 +94,9 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/entwicklung.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/entwicklung.js?v=202609141135" defer></script>
</body>
</html>
+4 -4
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
</head>
<body class="gate">
@@ -401,6 +401,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609120025" defer></script>
<script src="assets/js/gate.js?v=202609141135" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -377,11 +377,11 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/kalender.js?v=202609120025" defer></script>
<script src="assets/js/kalender-abo.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/kalender.js?v=202609141135" defer></script>
<script src="assets/js/kalender-abo.js?v=202609141135" defer></script>
</body>
</html>
+30 -19
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -288,6 +288,16 @@
<label class="feld-schild" for="netz-text">Tabelle</label>
<textarea id="netz-text" rows="6" class="netz__feld"
placeholder="Hier einfügen (Strg + V) …"></textarea>
<!-- ODER EINE DATEI (14.09.2026).
Filipes Backstage-Ausgabe ("Creator:innendaten …xlsx") enthält
ALLE Creator auf einmal – sie gehört damit hierher und nicht
in den Einzel-Import daneben. Der Knopf füllt einfach das Feld
darüber; ab da ist es derselbe Weg wie beim Einfügen. -->
<p class="feld-hinweis feld-hinweis--oder">
… oder direkt die Datei aus Backstage:
<input id="netz-datei" type="file" class="feld-datei"
accept=".xlsx,.csv,.txt,.tsv,text/csv,text/plain" />
</p>
</div>
<div class="neu__zeile neu__zeile--tag">
@@ -348,12 +358,13 @@
<form method="dialog" id="import-form" class="neu neu--blank">
<h2 class="dialog__titel">Aus dem Export einlesen</h2>
<p class="dialog__unter">
Eine Text-Tabelle (<strong>CSV</strong>) mit einer Zeile je Tag. Erst
siehst du, was passieren würde – geschrieben wird erst, wenn du
bestätigst. Dieselbe Datei zweimal einzulesen richtet keinen Schaden an.
<br><span class="dialog__unter--still">Eine Excel-Datei (.xlsx) passt hier
nicht hinein – dafür gibt es den Knopf „Backstage-Tabelle einfügen":
in Excel markieren, kopieren, dort einfügen.</span>
Eine Tabelle mit einer Zeile je Tag – <strong>Excel (.xlsx)</strong> oder
<strong>CSV</strong>. Erst siehst du, was passieren würde; geschrieben wird
erst, wenn du bestätigst. Dieselbe Datei zweimal einzulesen richtet keinen
Schaden an.
<br><span class="dialog__unter--still">Für eine Datei mit <em>allen</em>
Creator:innen (die Backstage-Ausgabe) nimm den Knopf „Backstage-Tabelle
einfügen" – dort geht sie auch als Datei.</span>
</p>
<div class="neu__zeile">
@@ -382,10 +393,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609120025"></script>
<script src="assets/js/bereiche.js?v=202609120025"></script>
<script src="assets/js/kopf.js?v=202609120025"></script>
<script src="assets/js/glocke.js?v=202609120025"></script>
<script src="assets/js/leistung.js?v=202609120025"></script>
<script src="assets/js/wahl.js?v=202609141135"></script>
<script src="assets/js/bereiche.js?v=202609141135"></script>
<script src="assets/js/kopf.js?v=202609141135"></script>
<script src="assets/js/glocke.js?v=202609141135"></script>
<script src="assets/js/leistung.js?v=202609141135"></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -423,10 +423,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/personen.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/personen.js?v=202609141135" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -214,11 +214,11 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/steckbrief.js?v=202609120025" defer></script>
<script src="assets/js/profil.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/steckbrief.js?v=202609141135" defer></script>
<script src="assets/js/profil.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -134,9 +134,9 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/rechte.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/rechte.js?v=202609141135" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/report.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/report.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -106,11 +106,11 @@
</main>
<script src="assets/js/ki.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/report.js?v=202609120025" defer></script>
<script src="assets/js/ki.js?v=202609141135" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/report.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -173,10 +173,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/scouting.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/scouting.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,21 +12,21 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -674,10 +674,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/start.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/start.js?v=202609141135" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -85,11 +85,11 @@
</main>
<script src="assets/js/ki.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/startcheck.js?v=202609120025" defer></script>
<script src="assets/js/ki.js?v=202609141135" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/startcheck.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -126,10 +126,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/steckbrief.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/steckbrief.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -94,9 +94,9 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/talente.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/talente.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/team.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/team.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -110,10 +110,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/team.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/team.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/team.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/team.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
@@ -84,9 +84,9 @@
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
es niemandem auf, und deshalb prueft pruef-css-klassen die
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/teamlage.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/teamlage.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -110,9 +110,9 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/treff-moderation.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/treff-moderation.js?v=202609141135" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,18 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -190,9 +190,9 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/treff-regeln.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/treff-regeln.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,22 +12,22 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -149,11 +149,11 @@
</main>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/uebersicht.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/uebersicht.js?v=202609141135" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609120025" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609120025" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609141135" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609141135" />
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
</head>
<body class="start">
@@ -190,10 +190,10 @@
</main>
<script src="assets/js/wahl.js?v=202609120025" defer></script>
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
<script src="assets/js/kopf.js?v=202609120025" defer></script>
<script src="assets/js/glocke.js?v=202609120025" defer></script>
<script src="assets/js/wissen.js?v=202609120025" defer></script>
<script src="assets/js/wahl.js?v=202609141135" defer></script>
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
<script src="assets/js/kopf.js?v=202609141135" defer></script>
<script src="assets/js/glocke.js?v=202609141135" defer></script>
<script src="assets/js/wissen.js?v=202609141135" defer></script>
</body>
</html>