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:
@@ -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, "&").replace(/</g, "<")}</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],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import { notbremse } from "./helfer-notbremse.mjs";
|
|||||||
notbremse(240_000, "pruef-backstage-import");
|
notbremse(240_000, "pruef-backstage-import");
|
||||||
|
|
||||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const { xlsxBauen } = await import("./helfer-xlsx-bauen.mjs");
|
||||||
const PORT = 4389;
|
const PORT = 4389;
|
||||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||||
const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-"));
|
const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-"));
|
||||||
@@ -455,6 +456,63 @@ console.log("\n=== Die Seite ===");
|
|||||||
{ path: join(HIER, "pruef-netz-leer.png") });
|
{ 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.click("#netz-zu");
|
||||||
await seite.waitForTimeout(300);
|
await seite.waitForTimeout(300);
|
||||||
/* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der
|
/* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der
|
||||||
@@ -466,61 +524,69 @@ console.log("\n=== Die Seite ===");
|
|||||||
ok(/Backstage/.test(netzSatz),
|
ok(/Backstage/.test(netzSatz),
|
||||||
"unter den Knöpfen steht dauerhaft, woher die Zahlen kommen");
|
"unter den Knöpfen steht dauerhaft, woher die Zahlen kommen");
|
||||||
|
|
||||||
/* ---- EINE EXCEL-DATEI IST EINE SACKGASSE MIT WEGWEISER ----------
|
/* ---- EINE EXCEL-DATEI WIRD GELESEN (14.09.2026) -----------------
|
||||||
Filipe: "meine excel datei kann ich nicht auswaehlen wieso??"
|
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
|
HIER STAND DAS GEGENTEIL, und es war richtig: Am 11.09. bekam eine
|
||||||
Wort dazu. Jetzt laesst sie sich waehlen und bekommt eine Antwort,
|
.xlsx eine Absage mit Wegweiser ("nimm den Knopf daneben"). Seit
|
||||||
die weiterhilft.
|
heute wird sie gelesen -- die Pruefung wurde deshalb GEDREHT, nicht
|
||||||
|
geloescht.
|
||||||
|
|
||||||
GEPRUEFT WIRD AN EINEM ECHTEN ZIP-KOPF ("PK"), denn genau das ist
|
GEBAUT WIRD EINE ECHTE DATEI, kein Stueck Text mit ZIP-Anfang. Der
|
||||||
eine .xlsx. Die Erkennung haengt am INHALT, nicht an der Endung --
|
erste Entwurf schickte zehn Bytes "PK…" und war gruen, weil der
|
||||||
deshalb heisst die Testdatei hier bewusst ".xlsx" UND traegt den
|
Server "ZIP-Ende fehlt" meldete -- das haette auch bei kaputtem
|
||||||
richtigen Kopf; eine Pruefung ueber die Endung waere gruen, ohne
|
Leser gestimmt. */
|
||||||
die Erkennung je zu beruehren. */
|
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.click("#import-oeffnen");
|
||||||
await seite.waitForTimeout(300);
|
await seite.waitForTimeout(300);
|
||||||
await seite.setInputFiles("#datei", {
|
await seite.setInputFiles("#datei", {
|
||||||
name: "backstage.xlsx",
|
name: "backstage.xlsx",
|
||||||
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
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(() => ({
|
const xlsx = await seite.evaluate(() => ({
|
||||||
text: document.getElementById("import-fehler")?.textContent || "",
|
fehler: document.getElementById("import-fehler")?.textContent || "",
|
||||||
vorschau: document.getElementById("vorschau")?.hidden,
|
vorschau: document.getElementById("vorschau")?.hidden,
|
||||||
los: document.getElementById("import-los")?.disabled,
|
text: document.getElementById("vorschau")?.textContent || "",
|
||||||
feld: document.getElementById("datei")?.value || "",
|
|
||||||
}));
|
}));
|
||||||
ok(/Excel/.test(xlsx.text), `es steht da, dass es eine Excel-Datei ist (${xlsx.text.slice(0, 50)}…)`);
|
ok(xlsx.fehler === "",
|
||||||
ok(/Backstage-Tabelle einfügen/.test(xlsx.text),
|
`die Excel-Datei wird ohne Absage gelesen ("${xlsx.fehler.slice(0, 50)}")`);
|
||||||
"und welcher Knopf stattdessen hilft");
|
ok(xlsx.vorschau === false, "und es entsteht eine Vorschau");
|
||||||
ok(/CSV/.test(xlsx.text), "und wie man sie umspeichert");
|
ok(xlsx.text.includes("Diamanten") && xlsx.text.includes("LIVE-Dauer"),
|
||||||
ok(xlsx.vorschau === true && xlsx.los === true,
|
`die Vorschau nennt die erkannten Spalten ("${xlsx.text.slice(0, 55)}…")`);
|
||||||
"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") });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GEGENPROBE: Eine echte Text-Tabelle geht durch denselben Weg
|
/* DIE ALTE .xls IST ETWAS ANDERES -- kein ZIP, sondern BIFF. Sie
|
||||||
weiterhin hindurch. Sonst hiesse das oben nur, dass gar nichts
|
bekommt weiterhin eine Absage, und die muss WEITERHELFEN. */
|
||||||
mehr eingelesen wird. */
|
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", {
|
await seite.setInputFiles("#datei", {
|
||||||
name: "tage.csv",
|
name: "tage.csv",
|
||||||
mimeType: "text/csv",
|
mimeType: "text/csv",
|
||||||
buffer: Buffer.from(`Datum;Diamanten;LIVE-Dauer (Minuten)
|
buffer: Buffer.from("Datum;Diamanten;LIVE-Dauer (Minuten)" + String.fromCharCode(10)
|
||||||
${gestern};1234;90
|
+ gestern + ";1234;90" + String.fromCharCode(10), "utf8"),
|
||||||
`, "utf8"),
|
|
||||||
});
|
});
|
||||||
await seite.waitForTimeout(900);
|
await seite.waitForTimeout(900);
|
||||||
const csv = await seite.evaluate(() => ({
|
const csv = await seite.evaluate(() => ({
|
||||||
fehler: document.getElementById("import-fehler")?.textContent || "",
|
fehler: document.getElementById("import-fehler")?.textContent || "",
|
||||||
vorschau: document.getElementById("vorschau")?.hidden,
|
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})`);
|
`eine CSV kommt weiterhin durch (Vorschau da: ${csv.vorschau === false})`);
|
||||||
await seite.click("#import-zu");
|
await seite.click("#import-zu");
|
||||||
await seite.waitForTimeout(250);
|
await seite.waitForTimeout(250);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import { xlsxLesen, tabelleAbKopf, alsText } from "./workspace-xlsx.js";
|
||||||
import {
|
import {
|
||||||
db, protokolliere, echteIp, sitzungLesen, istLeitung,
|
db, protokolliere, echteIp, sitzungLesen, istLeitung,
|
||||||
sichtbareCreatorIds, ROLLEN_SORTIERUNG, heuteLokal,
|
sichtbareCreatorIds, ROLLEN_SORTIERUNG, heuteLokal,
|
||||||
@@ -569,6 +570,102 @@ export function zahlLesen(roh) {
|
|||||||
return Number.isFinite(n) ? n : null;
|
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".
|
/** Macht aus einem Textwert ein Datum "JJJJ-MM-TT".
|
||||||
* Erkennt 2026-09-06, 06.09.2026 und 09/06/2026. */
|
* Erkennt 2026-09-06, 06.09.2026 und 09/06/2026. */
|
||||||
export function datumLesen(roh) {
|
export function datumLesen(roh) {
|
||||||
@@ -589,7 +686,21 @@ export function datumLesen(roh) {
|
|||||||
* Nur ein Vorschlag -- bestätigt wird er in der Vorschau. */
|
* Nur ein Vorschlag -- bestätigt wird er in der Vorschau. */
|
||||||
export function spaltenRaten(kopf) {
|
export function spaltenRaten(kopf) {
|
||||||
const muster = {
|
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,
|
diamanten: /diamant|diamond/i,
|
||||||
dauer_min: /dauer|duration|minut|zeit.*live|live.*zeit/i,
|
dauer_min: /dauer|duration|minut|zeit.*live|live.*zeit/i,
|
||||||
zuschauer_avg: /(durchschnitt|avg|average|⌀|ø).*(zuschauer|viewer)|(zuschauer|viewer).*(durchschnitt|avg)/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();
|
const rohWer = String(rohHandle).trim() || String(rohName).trim();
|
||||||
if (!rohWer) { schlecht.push({ zeile: i + 1, grund: "Keine Person in der Zeile." }); continue; }
|
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) { 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; }
|
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 };
|
const werte = { creator_id: p.id, name: p.name, tag };
|
||||||
let hatZahl = false;
|
let hatZahl = false;
|
||||||
for (const feld of messwerte) {
|
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;
|
werte[feld] = w;
|
||||||
if (w !== null && w !== undefined) hatZahl = true;
|
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)
|
DER WEG UEBER DIE TIKTOK-DATEI IST ENTFERNT (11.09.2026)
|
||||||
|
|
||||||
|
|||||||
@@ -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. `ä` und `ä` 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");
|
||||||
|
}
|
||||||
@@ -13,25 +13,25 @@
|
|||||||
"theme_color": "#06090f",
|
"theme_color": "#06090f",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609120025",
|
"src": "/assets/img/app-symbole/workspace-192.png?v=202609141135",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609120025",
|
"src": "/assets/img/app-symbole/workspace-512.png?v=202609141135",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"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",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"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",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
|
|||||||
@@ -417,6 +417,14 @@
|
|||||||
.feld-hinweis {
|
.feld-hinweis {
|
||||||
margin: 5px 0 0; font-size: .76rem; color: var(--text-still);
|
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
|
/* Ein Fenster, das laenger ist als der Bildschirm, versteckt seine
|
||||||
eigenen Knoepfe. Gerollt wird INNEN, damit "Uebernehmen" immer
|
eigenen Knoepfe. Gerollt wird INNEN, damit "Uebernehmen" immer
|
||||||
|
|||||||
+104
-13
@@ -476,26 +476,66 @@
|
|||||||
gelesen ergibt Zeichensalat -- und der schlimmste Ausgang waere
|
gelesen ergibt Zeichensalat -- und der schlimmste Ausgang waere
|
||||||
nicht "geht nicht", sondern eine Vorschau mit Zahlen, die
|
nicht "geht nicht", sondern eine Vorschau mit Zahlen, die
|
||||||
irgendwo aus dem Dateikopf stammen. */
|
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 anfang = new Uint8Array(await datei.slice(0, 8).arrayBuffer());
|
||||||
const beginnt = (...b) => b.every((x, i) => anfang[i] === x);
|
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 =
|
$('import-fehler').innerHTML =
|
||||||
'Das ist eine <strong>Excel-Datei</strong>. Hier passt eine Text-Tabelle '
|
'Das ist eine <strong>alte Excel-Datei (.xls)</strong>. Speichere sie in Excel '
|
||||||
+ '(CSV) hinein. Zwei Wege, beide dauern keine Minute:<br>'
|
+ 'einmal als <em>.xlsx</em> oder als <em>CSV UTF-8</em> – beides geht dann durch.';
|
||||||
+ '<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.';
|
|
||||||
$('datei').value = '';
|
$('datei').value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!ausExcel) {
|
||||||
importText = await datei.text();
|
try {
|
||||||
} catch {
|
importText = await datei.text();
|
||||||
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
|
} catch {
|
||||||
return;
|
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Eine leere Datei ist kein Lesefehler, sieht aber wie einer aus. */
|
/* Eine leere Datei ist kein Lesefehler, sieht aber wie einer aus. */
|
||||||
@@ -736,6 +776,57 @@
|
|||||||
$('netz-text').focus();
|
$('netz-text').focus();
|
||||||
});
|
});
|
||||||
$('netz-zu').addEventListener('click', () => $('netz-dialog').close());
|
$('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.
|
/* Beim EINFÜGEN sofort nachsehen -- nicht erst auf einen Knopf warten.
|
||||||
Wer eine Tabelle einfügt, will wissen, ob sie erkannt wurde; ein
|
Wer eine Tabelle einfügt, will wissen, ob sie erkannt wurde; ein
|
||||||
zweiter Klick dazwischen ist nur eine Gelegenheit, es nicht zu tun. */
|
zweiter Klick dazwischen ist nur eine Gelegenheit, es nicht zu tun. */
|
||||||
|
|||||||
+12
-12
@@ -12,19 +12,19 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -252,10 +252,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/aufgaben.js?v=202609120025" defer></script>
|
<script src="assets/js/aufgaben.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/automation.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -147,10 +147,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/automation.js?v=202609120025" defer></script>
|
<script src="assets/js/automation.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+17
-17
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/bereich.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -181,14 +181,14 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/checkliste.js?v=202609120025" defer></script>
|
<script src="assets/js/checkliste.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/ampel.js?v=202609120025" defer></script>
|
<script src="assets/js/ampel.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/schulung.js?v=202609120025" defer></script>
|
<script src="assets/js/schulung.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/vorlagen.js?v=202609120025" defer></script>
|
<script src="assets/js/vorlagen.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereich.js?v=202609120025" defer></script>
|
<script src="assets/js/bereich.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/calls.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -89,11 +89,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609120025" defer></script>
|
<script src="assets/js/ki.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/calls.js?v=202609120025" defer></script>
|
<script src="assets/js/calls.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,26 +12,26 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||||
Dialog zu sehen bekam. -->
|
Dialog zu sehen bekam. -->
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/chat.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -341,10 +341,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/bereiche.js?v=202609120025"></script>
|
<script src="assets/js/bereiche.js?v=202609141135"></script>
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025"></script>
|
<script src="assets/js/kopf.js?v=202609141135"></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025"></script>
|
<script src="assets/js/glocke.js?v=202609141135"></script>
|
||||||
<script src="assets/js/chat.js?v=202609120025"></script>
|
<script src="assets/js/chat.js?v=202609141135"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+15
-15
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/content.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/content.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -179,12 +179,12 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/checkliste.js?v=202609120025" defer></script>
|
<script src="assets/js/checkliste.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/vorlagen.js?v=202609120025" defer></script>
|
<script src="assets/js/vorlagen.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/content.js?v=202609120025" defer></script>
|
<script src="assets/js/content.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -36,15 +36,15 @@
|
|||||||
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
|
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
|
||||||
type="image/webp" media="(max-width: 899px)" />
|
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="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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
|
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
|
||||||
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
|
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
|
||||||
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
|
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
|
||||||
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
|
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
|
||||||
die eine Seite, auf der man das Umfaerben sieht. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="gate gate--crew">
|
<body class="gate gate--crew">
|
||||||
@@ -319,6 +319,6 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/gate.js?v=202609120025" defer></script>
|
<script src="assets/js/gate.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,25 +13,25 @@
|
|||||||
"theme_color": "#06090f",
|
"theme_color": "#06090f",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/crew-192.png?v=202609120025",
|
"src": "/assets/img/app-symbole/crew-192.png?v=202609141135",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/crew-512.png?v=202609120025",
|
"src": "/assets/img/app-symbole/crew-512.png?v=202609141135",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"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",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"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",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/dateien.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -108,10 +108,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/dateien.js?v=202609120025" defer></script>
|
<script src="assets/js/dateien.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -7,14 +7,14 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/entwicklung.js?v=202609120025" defer></script>
|
<script src="assets/js/entwicklung.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
|
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
|
||||||
type="image/webp" media="(max-width: 899px)" />
|
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="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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="gate">
|
<body class="gate">
|
||||||
@@ -401,6 +401,6 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/gate.js?v=202609120025" defer></script>
|
<script src="assets/js/gate.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/kalender.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -377,11 +377,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kalender.js?v=202609120025" defer></script>
|
<script src="assets/js/kalender.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kalender-abo.js?v=202609120025" defer></script>
|
<script src="assets/js/kalender-abo.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+30
-19
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/leistung.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -288,6 +288,16 @@
|
|||||||
<label class="feld-schild" for="netz-text">Tabelle</label>
|
<label class="feld-schild" for="netz-text">Tabelle</label>
|
||||||
<textarea id="netz-text" rows="6" class="netz__feld"
|
<textarea id="netz-text" rows="6" class="netz__feld"
|
||||||
placeholder="Hier einfügen (Strg + V) …"></textarea>
|
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>
|
||||||
|
|
||||||
<div class="neu__zeile neu__zeile--tag">
|
<div class="neu__zeile neu__zeile--tag">
|
||||||
@@ -348,12 +358,13 @@
|
|||||||
<form method="dialog" id="import-form" class="neu neu--blank">
|
<form method="dialog" id="import-form" class="neu neu--blank">
|
||||||
<h2 class="dialog__titel">Aus dem Export einlesen</h2>
|
<h2 class="dialog__titel">Aus dem Export einlesen</h2>
|
||||||
<p class="dialog__unter">
|
<p class="dialog__unter">
|
||||||
Eine Text-Tabelle (<strong>CSV</strong>) mit einer Zeile je Tag. Erst
|
Eine Tabelle mit einer Zeile je Tag – <strong>Excel (.xlsx)</strong> oder
|
||||||
siehst du, was passieren würde – geschrieben wird erst, wenn du
|
<strong>CSV</strong>. Erst siehst du, was passieren würde; geschrieben wird
|
||||||
bestätigst. Dieselbe Datei zweimal einzulesen richtet keinen Schaden an.
|
erst, wenn du bestätigst. Dieselbe Datei zweimal einzulesen richtet keinen
|
||||||
<br><span class="dialog__unter--still">Eine Excel-Datei (.xlsx) passt hier
|
Schaden an.
|
||||||
nicht hinein – dafür gibt es den Knopf „Backstage-Tabelle einfügen":
|
<br><span class="dialog__unter--still">Für eine Datei mit <em>allen</em>
|
||||||
in Excel markieren, kopieren, dort einfügen.</span>
|
Creator:innen (die Backstage-Ausgabe) nimm den Knopf „Backstage-Tabelle
|
||||||
|
einfügen" – dort geht sie auch als Datei.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="neu__zeile">
|
<div class="neu__zeile">
|
||||||
@@ -382,10 +393,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025"></script>
|
<script src="assets/js/wahl.js?v=202609141135"></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025"></script>
|
<script src="assets/js/bereiche.js?v=202609141135"></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025"></script>
|
<script src="assets/js/kopf.js?v=202609141135"></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025"></script>
|
<script src="assets/js/glocke.js?v=202609141135"></script>
|
||||||
<script src="assets/js/leistung.js?v=202609120025"></script>
|
<script src="assets/js/leistung.js?v=202609141135"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/personen.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -423,10 +423,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/personen.js?v=202609120025" defer></script>
|
<script src="assets/js/personen.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/profil.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -214,11 +214,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/steckbrief.js?v=202609120025" defer></script>
|
<script src="assets/js/steckbrief.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/profil.js?v=202609120025" defer></script>
|
<script src="assets/js/profil.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -7,14 +7,14 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -134,9 +134,9 @@
|
|||||||
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
||||||
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
||||||
die seit dem 07.09. jede Seite danach absucht. -->
|
die seit dem 07.09. jede Seite danach absucht. -->
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/rechte.js?v=202609120025" defer></script>
|
<script src="assets/js/rechte.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/report.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/report.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -106,11 +106,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609120025" defer></script>
|
<script src="assets/js/ki.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/report.js?v=202609120025" defer></script>
|
<script src="assets/js/report.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/scouting.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -173,10 +173,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/scouting.js?v=202609120025" defer></script>
|
<script src="assets/js/scouting.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -12,21 +12,21 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
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/heim.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -674,10 +674,10 @@
|
|||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/start.js?v=202609120025" defer></script>
|
<script src="assets/js/start.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -85,11 +85,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609120025" defer></script>
|
<script src="assets/js/ki.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/startcheck.js?v=202609120025" defer></script>
|
<script src="assets/js/startcheck.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/profil.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -126,10 +126,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/steckbrief.js?v=202609120025" defer></script>
|
<script src="assets/js/steckbrief.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -7,14 +7,14 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/talente.js?v=202609120025" defer></script>
|
<script src="assets/js/talente.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -7,20 +7,20 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/team.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/team.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -110,10 +110,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/team.js?v=202609120025" defer></script>
|
<script src="assets/js/team.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -7,20 +7,20 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/team.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/team.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
|
<!-- `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
|
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
|
||||||
es niemandem auf, und deshalb prueft pruef-css-klassen die
|
es niemandem auf, und deshalb prueft pruef-css-klassen die
|
||||||
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
|
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/teamlage.js?v=202609120025" defer></script>
|
<script src="assets/js/teamlage.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/haus.css?v=202609141135" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -110,9 +110,9 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/treff-moderation.js?v=202609120025" defer></script>
|
<script src="assets/js/treff-moderation.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -7,18 +7,18 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/treff.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
|
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
|
||||||
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
|
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
|
||||||
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
|
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
|
||||||
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -190,9 +190,9 @@
|
|||||||
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
||||||
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
||||||
die seit dem 07.09. jede Seite danach absucht. -->
|
die seit dem 07.09. jede Seite danach absucht. -->
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/treff-regeln.js?v=202609120025" defer></script>
|
<script src="assets/js/treff-regeln.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,22 +12,22 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||||
Dialog "Creator anlegen". -->
|
Dialog "Creator anlegen". -->
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -149,11 +149,11 @@
|
|||||||
|
|
||||||
</main>
|
</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/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/uebersicht.js?v=202609120025" defer></script>
|
<script src="assets/js/uebersicht.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-13
@@ -12,20 +12,20 @@
|
|||||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||||
eine Flaeche sind statt zweier. -->
|
eine Flaeche sind statt zweier. -->
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?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=202609120025" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/wissen.css?v=202609141135" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609120025" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609141135" />
|
||||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -190,10 +190,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609120025" defer></script>
|
<script src="assets/js/wahl.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609120025" defer></script>
|
<script src="assets/js/bereiche.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609120025" defer></script>
|
<script src="assets/js/kopf.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609120025" defer></script>
|
<script src="assets/js/glocke.js?v=202609141135" defer></script>
|
||||||
<script src="assets/js/wissen.js?v=202609120025" defer></script>
|
<script src="assets/js/wissen.js?v=202609141135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user