Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
621 lines
26 KiB
JavaScript
621 lines
26 KiB
JavaScript
/* =====================================================================
|
||
material.js — Bilder und Videos zum Posten, jedes nur einmal
|
||
=====================================================================
|
||
|
||
Auftrag vom 22.09.2026.
|
||
|
||
DIESE DATEI KENNT KEINEN ROLLENNAMEN. Ob jemand einstellen darf und
|
||
ob er sieht, wer sich etwas genommen hat, sagt der Server
|
||
(`darf_einstellen`, `sieht_wer`). Stünde es hier, stünde es in einer
|
||
Datei, die jeder herunterlädt — und wäre am Tag der nächsten
|
||
Änderung falsch.
|
||
|
||
DER WICHTIGSTE TEIL IST DAS NEHMEN, und es sind zwei Schritte:
|
||
erst festschreiben (`/nehmen`), dann herunterladen. Wer zu spät
|
||
kommt, bekommt 409 und sieht sofort, dass es weg ist — statt eine
|
||
Datei zu laden, die schon jemand anders postet.
|
||
===================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (was, klasse, text) => {
|
||
const e = document.createElement(was);
|
||
if (klasse) e.className = klasse;
|
||
if (text !== undefined) e.textContent = text;
|
||
return e;
|
||
};
|
||
const melde = (t) => { const f = $('fehler'); if (f) f.textContent = t || ''; };
|
||
|
||
let stand = null;
|
||
|
||
/* Eine Größe, die ein Mensch lesen kann. */
|
||
function groesse(bytes) {
|
||
const mb = bytes / (1024 * 1024);
|
||
if (mb >= 1) return `${mb.toFixed(1).replace('.', ',')} MB`;
|
||
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||
}
|
||
|
||
function datum(roh) {
|
||
try {
|
||
const d = new Date(roh);
|
||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||
} catch { return ''; }
|
||
}
|
||
|
||
/* EIN PUNKT, ABER KEIN ZWEITER. Eine deutsche Datumsangabe endet
|
||
selbst auf einen Punkt ("22.09."). Ein angehängter Satzpunkt
|
||
ergibt dann "am 22.09.." — auf dem Bildschirm sieht das nach
|
||
Tippfehler aus. Gefunden beim ersten Blick mit echten Augen,
|
||
nicht in einer Messung: Kein Prüfprogramm hätte danach gefragt. */
|
||
const punkt = (t) => (/[.!?]$/.test(t) ? t : t + '.');
|
||
|
||
|
||
/* ===================================================================
|
||
DIE VIER ZUSTÄNDE (22.09.2026)
|
||
-------------------------------------------------------------------
|
||
Filipe: „mach kategorien welche sind benutzt welche nicht welche
|
||
sind noch offen, welche laufen von wan bis wann."
|
||
|
||
Gerechnet wird der Zustand auf dem SERVER (`zustandVon`), nicht
|
||
hier. Zwei Rechnungen für dieselbe Frage laufen auseinander — und
|
||
spätestens um Mitternacht zeigte die Seite einen anderen Zustand
|
||
als die Datenbank.
|
||
|
||
Die Reihenfolge hier ist die, in der man sie liest: erst was jetzt
|
||
geht, dann was kommt, dann was vorbei ist, dann was weg ist.
|
||
=================================================================== */
|
||
const ZUSTAENDE = [
|
||
{ wert: '', wort: 'Alle' },
|
||
{ wert: 'frei', wort: 'Jetzt frei' },
|
||
{ wert: 'spaeter', wort: 'Kommt noch' },
|
||
{ wert: 'abgelaufen', wort: 'Abgelaufen' },
|
||
{ wert: 'benutzt', wort: 'Schon benutzt' },
|
||
];
|
||
const ZUSTAND_WORT = Object.fromEntries(
|
||
ZUSTAENDE.filter((z) => z.wert).map((z) => [z.wert, z.wort]));
|
||
|
||
/* Welche Kategorie gerade gewählt ist. Steht außerhalb von `laden()`:
|
||
Wäre sie drin, spränge die Liste nach jedem Nehmen zurück auf
|
||
„Alle" — und man müsste jedes Mal neu suchen, wo man war. */
|
||
let katFilter = '';
|
||
|
||
function tagText(iso) {
|
||
if (!iso) return '';
|
||
const t = String(iso).slice(0, 10).split('-');
|
||
return t.length === 3 ? `${t[2]}.${t[1]}.${t[0]}` : String(iso);
|
||
}
|
||
|
||
/* „ab 01.10.2026", „bis 07.10.2026" oder „01.10. – 07.10.2026".
|
||
Ein Zeitraum, bei dem eine Seite fehlt, ist trotzdem einer — und
|
||
„ab 01.10." sagt mehr als ein Gedankenstrich ins Leere. */
|
||
function zeitraum(m) {
|
||
if (m.gilt_ab && m.gilt_bis) return `${tagText(m.gilt_ab)} – ${tagText(m.gilt_bis)}`;
|
||
if (m.gilt_ab) return `ab ${tagText(m.gilt_ab)}`;
|
||
if (m.gilt_bis) return `bis ${tagText(m.gilt_bis)}`;
|
||
return '';
|
||
}
|
||
|
||
/* ===================================================================
|
||
BEARBEITEN — ein Formular IN der Karte, kein Kasten davor
|
||
-------------------------------------------------------------------
|
||
Erster Entwurf stellte drei Nachfragen hintereinander: Text, dann
|
||
„gilt ab", dann „gilt bis". Das ist dreimal Lesen und dreimal
|
||
Bestätigen für eine Sache, die auf eine Karte passt — und wer beim
|
||
dritten abbricht, hat die ersten zwei umsonst getippt.
|
||
|
||
Zweiter Fund beim Messen der echten Schnittstelle: `frageNach`
|
||
kennt kein vorbelegtes Textfeld. Eine Bearbeitung, die den
|
||
bisherigen Text nicht zeigt, ist keine Bearbeitung, sondern ein
|
||
Neuschreiben.
|
||
|
||
Also hier: Die Karte klappt auf, drei Felder stehen mit ihren
|
||
heutigen Werten darin, ein Knopf speichert. Abbrechen schließt
|
||
wieder — es wurde nichts angefasst, deshalb braucht es auch keine
|
||
Rückfrage.
|
||
=================================================================== */
|
||
function bearbeiten(m) {
|
||
const karteEl = document.querySelector(`.m-karte[data-id="${m.id}"]`);
|
||
if (!karteEl || karteEl.querySelector('.m-bearb')) return;
|
||
|
||
const f = el('form', 'm-bearb');
|
||
const zeile = (schild, art, wert, id) => {
|
||
const d = el('div', 'm-bearb__feld');
|
||
const l = el('label', 'feld-schild', schild);
|
||
l.htmlFor = id;
|
||
const i = art === 'text'
|
||
? el('textarea') : el('input');
|
||
i.id = id;
|
||
if (art !== 'text') i.type = art;
|
||
i.value = wert || '';
|
||
if (art === 'text') { i.rows = 2; i.maxLength = 600; }
|
||
d.append(l, i);
|
||
f.append(d);
|
||
return i;
|
||
};
|
||
const tText = zeile('Wofür ist es?', 'text', m.text, `b-text-${m.id}`);
|
||
const tAb = zeile('Gilt ab (freiwillig)', 'date', m.gilt_ab, `b-ab-${m.id}`);
|
||
const tBis = zeile('Gilt bis (freiwillig)', 'date', m.gilt_bis, `b-bis-${m.id}`);
|
||
|
||
const hinweis = el('p', 'm-bearb__hinweis',
|
||
'Die Datei selbst bleibt, wie sie ist. Wer ein anderes Bild will, '
|
||
+ 'stellt ein neues Stück bereit.');
|
||
const fehlerZeile = el('p', 'm-bearb__fehler');
|
||
fehlerZeile.setAttribute('role', 'alert');
|
||
|
||
const knoepfe = el('p', 'm-bearb__knoepfe');
|
||
const speichern = el('button', 'knopf knopf--klein', 'Speichern');
|
||
speichern.type = 'submit';
|
||
const ab = el('button', 'knopf-still', 'Abbrechen');
|
||
ab.type = 'button';
|
||
ab.addEventListener('click', () => f.remove());
|
||
knoepfe.append(speichern, ab);
|
||
f.append(hinweis, fehlerZeile, knoepfe);
|
||
|
||
f.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault();
|
||
/* VORHER PRUEFEN, WAS DER SERVER OHNEHIN PRUEFT. Nicht statt
|
||
seiner — er bleibt die Regel. Aber ein Fenster, das rückwärts
|
||
läuft, hier schon zu sagen, spart den Weg hin und zurück. */
|
||
if (tAb.value && tBis.value && tAb.value > tBis.value) {
|
||
fehlerZeile.textContent = 'Das Ende liegt vor dem Anfang. Dreh die zwei Daten um.';
|
||
tBis.focus();
|
||
return;
|
||
}
|
||
speichern.disabled = true;
|
||
try {
|
||
const a = await fetch(`/workspace/api/material/${m.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
text: tText.value, gilt_ab: tAb.value, gilt_bis: tBis.value,
|
||
}),
|
||
});
|
||
const r = await a.json().catch(() => ({}));
|
||
if (!a.ok) {
|
||
fehlerZeile.textContent = window.sagWas?.(r.fehler, 'Ging nicht.') || 'Ging nicht.';
|
||
speichern.disabled = false;
|
||
return;
|
||
}
|
||
await laden();
|
||
} catch {
|
||
fehlerZeile.textContent = 'Das hat gerade nicht geklappt. Versuch es gleich noch einmal.';
|
||
speichern.disabled = false;
|
||
}
|
||
});
|
||
|
||
karteEl.querySelector('.m-karte__koerper')?.append(f);
|
||
tText.focus();
|
||
}
|
||
|
||
async function entfernen(m) {
|
||
/* `frageNach` liefert bei einer reinen Rückfrage `true`, nicht
|
||
`{ ok: true }` — das Objekt kommt nur, wenn ein Feld dabei ist.
|
||
Der erste Entwurf prüfte auf `erg?.ok`; damit wäre „Entfernen"
|
||
ein Knopf gewesen, der nichts tut, und niemand hätte den Grund
|
||
gesehen. Gemessen an der Quelle, nicht angenommen. */
|
||
const erg = await window.frageNach?.({
|
||
titel: 'Wirklich entfernen?',
|
||
was: `„${m.name}“ verschwindet aus der Liste, und die Datei wird gelöscht.`,
|
||
bleibt: 'Was schon jemand genommen hat, ist davon nicht betroffen — '
|
||
+ 'das liegt längst auf seinem Gerät.',
|
||
endgueltig: true,
|
||
ja: 'Entfernen', nein: 'Doch nicht',
|
||
});
|
||
if (!erg) return;
|
||
try {
|
||
const a = await fetch(`/workspace/api/material/${m.id}`, { method: 'DELETE' });
|
||
const r = await a.json().catch(() => ({}));
|
||
if (!a.ok) { melde(window.sagWas?.(r.fehler, 'Ging nicht.') || 'Ging nicht.'); return; }
|
||
melde('');
|
||
await laden();
|
||
} catch { melde('Das hat gerade nicht geklappt. Versuch es gleich noch einmal.'); }
|
||
}
|
||
|
||
/* ===================================================================
|
||
DIE KATEGORIEN
|
||
-------------------------------------------------------------------
|
||
Die Zahl neben jedem Wort kommt aus den echten Daten. Eine Zahl im
|
||
HTML wäre die, die beim nächsten Stück falsch dasteht.
|
||
|
||
EINE KATEGORIE OHNE INHALT WIRD NICHT VERSTECKT, sondern grau: Wer
|
||
„Abgelaufen" sucht und den Knopf nicht findet, weiß nicht, ob es
|
||
nichts gibt oder ob er ihn übersieht. Eine 0 ist eine Antwort.
|
||
=================================================================== */
|
||
function katLeisteBauen(alle) {
|
||
const ziel = $('m-kat');
|
||
if (!ziel) return;
|
||
ziel.replaceChildren();
|
||
for (const z of ZUSTAENDE) {
|
||
const n = z.wert ? alle.filter((m) => m.zustand === z.wert).length : alle.length;
|
||
const b = el('button', 'schritt m-kat__knopf');
|
||
b.type = 'button';
|
||
b.dataset.wert = z.wert;
|
||
b.setAttribute('aria-pressed', String(katFilter === z.wert));
|
||
if (!n) b.dataset.leer = 'ja';
|
||
/* Das Wort als blosser Text, nicht in einer eigenen Huelle: Die
|
||
Huelle haette einen Klassennamen ohne eine einzige Regel
|
||
dahinter -- und niemand weiss beim naechsten Umbau, ob er sie
|
||
anfassen darf. Anonymer Text in einer Flex-Reihe ist selbst
|
||
ein Flex-Kind, `gap` wirkt also trotzdem. */
|
||
b.append(document.createTextNode(z.wort));
|
||
b.append(el('span', 'm-kat__zahl', String(n)));
|
||
b.addEventListener('click', () => {
|
||
/* NOCH EINMAL AUF DASSELBE HEISST „ALLE". Sonst gibt es keinen
|
||
Weg zurueck ausser dem Knopf ganz links -- und wer zweimal
|
||
tippt, erwartet das Gegenteil, nicht nichts.
|
||
„Alle" selbst bleibt dabei „Alle": `'' === ''` waere sonst
|
||
ein Klick, der den Zustand nicht aendert und trotzdem neu
|
||
zeichnet. */
|
||
katFilter = (z.wert && katFilter === z.wert) ? '' : z.wert;
|
||
zeichnen();
|
||
});
|
||
ziel.append(b);
|
||
}
|
||
}
|
||
|
||
/* ===================================================================
|
||
EINE KARTE
|
||
=================================================================== */
|
||
function karte(m) {
|
||
const k = el('article', 'm-karte');
|
||
k.dataset.id = String(m.id);
|
||
if (m.genommen) k.dataset.genommen = 'ja';
|
||
|
||
/* DIE VORSCHAU IST DIE HAUPTSACHE. Wer entscheiden soll, ob er
|
||
etwas postet, muss es sehen -- ein Dateiname reicht dafür nicht.
|
||
|
||
SIE LÄDT ERST BEIM SICHTBARWERDEN (`loading="lazy"`): Bei
|
||
dreißig Bildern wären das sonst dreißig Abrufe auf einmal, und
|
||
am Handy im Funkloch sieht man eine Minute lang nichts. */
|
||
const schau = el('div', 'm-karte__schau');
|
||
if (m.genommen) {
|
||
/* Was weg ist, wird nicht mehr ausgeliefert -- der Server gibt
|
||
die Datei nur dem, der sie genommen hat. Statt eines kaputten
|
||
Bildes steht hier ein ehrliches Feld. */
|
||
schau.append(el('span', 'm-karte__weg-zeichen', m.art === 'video' ? '▣' : '▤'));
|
||
} else if (m.art === 'bild') {
|
||
const bild = el('img', 'm-karte__bild');
|
||
bild.src = `/workspace/api/material/${m.id}/vorschau`;
|
||
bild.alt = '';
|
||
bild.loading = 'lazy';
|
||
bild.decoding = 'async';
|
||
schau.append(bild);
|
||
} else {
|
||
const v = el('span', 'm-karte__art', 'Video');
|
||
schau.append(v);
|
||
}
|
||
k.append(schau);
|
||
|
||
const koerper = el('div', 'm-karte__koerper');
|
||
koerper.append(el('p', 'm-karte__name', m.name));
|
||
if (m.text) koerper.append(el('p', 'm-karte__text', m.text));
|
||
|
||
const fuss = el('p', 'm-karte__fuss');
|
||
fuss.append(el('span', 'm-karte__marke', m.art === 'video' ? 'Video' : 'Bild'));
|
||
fuss.append(el('span', 'm-karte__leise', groesse(m.groesse)));
|
||
fuss.append(el('span', 'm-karte__leise', `von ${m.von}`));
|
||
koerper.append(fuss);
|
||
|
||
/* VON WANN BIS WANN (22.09.2026).
|
||
Steht nur da, WENN es ein Fenster gibt. Eine Zeile "ohne
|
||
Zeitraum" auf jeder zweiten Karte waere Rauschen -- und Rauschen
|
||
liest niemand, auch nicht die eine Zeile, auf die es ankommt. */
|
||
if (m.gilt_ab || m.gilt_bis) {
|
||
const z = el('p', 'm-karte__zeit');
|
||
z.dataset.zustand = m.zustand;
|
||
z.append(el('span', 'm-karte__zeit-marke', ZUSTAND_WORT[m.zustand] || ''));
|
||
z.append(document.createTextNode(' ' + zeitraum(m)));
|
||
koerper.append(z);
|
||
}
|
||
|
||
/* ---- Der Zustand ---- */
|
||
const meldung = el('p', 'm-karte__meldung');
|
||
meldung.hidden = true;
|
||
|
||
if (m.genommen) {
|
||
/* WER ES HAT, STEHT NUR FÜR ZWEI DA. Der Server schickt das Feld
|
||
den anderen gar nicht erst mit -- diese Zeile prüft deshalb
|
||
nicht auf eine Rolle, sondern darauf, ob die Angabe da ist. */
|
||
if (m.von_mir_genommen) {
|
||
koerper.append(el('p', 'm-karte__stand m-karte__stand--meins',
|
||
'Du hast es genommen.'));
|
||
const a = el('a', 'knopf knopf--klein', 'Noch einmal laden');
|
||
a.href = `/workspace/api/material/${m.id}/datei`;
|
||
a.setAttribute('download', m.name);
|
||
koerper.append(a);
|
||
} else if (m.genommen_von) {
|
||
koerper.append(el('p', 'm-karte__stand', punkt(
|
||
`${m.genommen_von} hat es genommen${m.genommen_am ? ' am ' + datum(m.genommen_am) : ''}`)));
|
||
} else {
|
||
koerper.append(el('p', 'm-karte__stand', 'Schon benutzt.'));
|
||
}
|
||
} else {
|
||
const knopf = el('button', 'knopf knopf--klein', 'Nehmen und laden');
|
||
knopf.type = 'button';
|
||
knopf.addEventListener('click', () => nehmen(m, knopf, meldung));
|
||
koerper.append(knopf);
|
||
}
|
||
|
||
/* BEARBEITEN UND ENTFERNEN (22.09.2026).
|
||
Filipe: "will ich dass die rechte und linke hand es auch
|
||
bearbeiten und loeschen koennen. dogfather auch fals es ein
|
||
fehler gab."
|
||
|
||
OB JEMAND DARF, SAGT DER SERVER (`m.darf_ich`) -- hier wird es
|
||
nicht nachgerechnet. Zwei Rechnungen fuer dieselbe Frage laufen
|
||
auseinander, und die im Browser ist die, der man ohnehin nicht
|
||
trauen darf.
|
||
|
||
Ein vergebenes Stueck bleibt unangetastet: Wer es genommen hat,
|
||
hat sich auf diesen Text verlassen. */
|
||
if (m.darf_ich && !m.genommen) {
|
||
const reihe = el('p', 'm-karte__verwalten');
|
||
const bearb = el('button', 'knopf-still', 'Bearbeiten');
|
||
bearb.type = 'button';
|
||
bearb.addEventListener('click', () => bearbeiten(m));
|
||
const weg = el('button', 'knopf-still knopf-still--warn', 'Entfernen');
|
||
weg.type = 'button';
|
||
weg.addEventListener('click', () => entfernen(m));
|
||
reihe.append(bearb, weg);
|
||
koerper.append(reihe);
|
||
}
|
||
|
||
koerper.append(meldung);
|
||
k.append(koerper);
|
||
return k;
|
||
}
|
||
|
||
/* ===================================================================
|
||
NEHMEN — erst festschreiben, dann laden
|
||
=================================================================== */
|
||
async function nehmen(m, knopf, meldung) {
|
||
knopf.disabled = true;
|
||
knopf.textContent = 'einen Moment …';
|
||
try {
|
||
const a = await fetch(`/workspace/api/material/${m.id}/nehmen`, { method: 'POST' });
|
||
if (a.status === 409) {
|
||
/* DER WICHTIGE FALL: Jemand war schneller. Kein Fehler, keine
|
||
rote Zeile -- eine Auskunft. Und die Liste wird neu geholt,
|
||
damit man sofort sieht, was noch da ist. */
|
||
const j = await a.json().catch(() => ({}));
|
||
meldung.textContent = j.wer
|
||
? `${j.wer} war ein paar Sekunden schneller.`
|
||
: 'Da war jemand schneller — das ist schon vergeben.';
|
||
meldung.hidden = false;
|
||
knopf.remove();
|
||
setTimeout(laden, 1400);
|
||
return;
|
||
}
|
||
if (!a.ok) {
|
||
const j = await a.json().catch(() => ({}));
|
||
meldung.textContent = window.sagWas?.(j.fehler, 'Ging gerade nicht.')
|
||
|| 'Ging gerade nicht.';
|
||
meldung.hidden = false;
|
||
knopf.disabled = false;
|
||
knopf.textContent = 'Nehmen und laden';
|
||
return;
|
||
}
|
||
|
||
/* ES GEHÖRT JETZT IHM. Der Download wird ausgelöst, ohne die
|
||
Seite zu verlassen -- ein `location.assign` hätte die Liste
|
||
weggeworfen, und danach wüsste niemand mehr, was er gerade
|
||
geholt hat. */
|
||
const weg = (await a.json().catch(() => ({}))).weg
|
||
|| `/workspace/api/material/${m.id}/datei`;
|
||
const link = document.createElement('a');
|
||
link.href = weg;
|
||
link.download = m.name;
|
||
document.body.append(link);
|
||
link.click();
|
||
link.remove();
|
||
|
||
meldung.textContent = 'Gehört dir. Der Download läuft.';
|
||
meldung.hidden = false;
|
||
knopf.remove();
|
||
setTimeout(laden, 1200);
|
||
} catch {
|
||
meldung.textContent = 'Das Netz war gerade weg. Versuch es noch einmal.';
|
||
meldung.hidden = false;
|
||
knopf.disabled = false;
|
||
knopf.textContent = 'Nehmen und laden';
|
||
}
|
||
}
|
||
|
||
/* ===================================================================
|
||
EINSTELLEN
|
||
=================================================================== */
|
||
function einstellenAufbauen() {
|
||
const knopf = $('m-hoch');
|
||
if (!knopf) return;
|
||
knopf.addEventListener('click', async () => {
|
||
const feld = $('m-datei');
|
||
const datei = feld?.files?.[0];
|
||
const stand2 = $('m-stand');
|
||
if (!datei) {
|
||
stand2.textContent = 'Such erst ein Bild oder Video aus.';
|
||
return;
|
||
}
|
||
if (stand?.max_bytes && datei.size > stand.max_bytes) {
|
||
stand2.textContent = `Das ist zu groß (${groesse(datei.size)}). `
|
||
+ `Mehr als ${groesse(stand.max_bytes)} geht nicht.`;
|
||
return;
|
||
}
|
||
|
||
/* DAS FENSTER WIRD VOR DEM HOCHLADEN GEPRÜFT, NICHT DANACH.
|
||
Der Server lehnt ein verkehrtes Fenster ohnehin ab — aber
|
||
erst, nachdem 60 MB durch das Handynetz gewandert sind. Wer
|
||
das erlebt, lädt zweimal hoch für einen Zahlendreher. */
|
||
const abFeld = $('m-ab');
|
||
const bisFeld = $('m-bis');
|
||
if (abFeld?.value && bisFeld?.value && abFeld.value > bisFeld.value) {
|
||
stand2.textContent = window.sagWas?.('fenster_verkehrt', '')
|
||
|| 'Das Ende liegt vor dem Anfang.';
|
||
bisFeld.focus();
|
||
return;
|
||
}
|
||
|
||
knopf.disabled = true;
|
||
|
||
/* ÜBER DEN GEMEINSAMEN HOCHLADER -- er zeigt den Fortschritt und
|
||
lässt sich abbrechen. Genau dafür wurde er am 21.09. gebaut;
|
||
eine eigene Fassung hier wäre die zweite, die es nicht kann.
|
||
|
||
DIE SCHNITTSTELLE IST NACHGELESEN, NICHT GERATEN: `hochladen()`
|
||
nimmt (Pfad, Daten, {kopf, beiFortschritt}) und liefert
|
||
{fertig, abbrechen}; `fertig` löst mit {ok, status, daten} auf.
|
||
Mein erster Entwurf hatte sie sich ausgedacht. */
|
||
const anzeige = window.hochladeAnzeige?.($('m-fortschritt'), datei.name,
|
||
() => lauf?.abbrechen());
|
||
const lauf = window.hochladen?.('/workspace/api/material', datei, {
|
||
kopf: {
|
||
'Content-Type': datei.type || 'application/octet-stream',
|
||
'X-Datei-Name': encodeURIComponent(datei.name || ''),
|
||
'X-Text': encodeURIComponent(($('m-text')?.value || '').slice(0, 600)),
|
||
/* Leer ist erlaubt und heißt „gilt immer" — der Server macht
|
||
aus einer leeren Kopfzeile `null`, nicht einen Fehler. */
|
||
'X-Gilt-Ab': abFeld?.value || '',
|
||
'X-Gilt-Bis': bisFeld?.value || '',
|
||
},
|
||
beiFortschritt: (anteil, gesendet, gesamt) => anzeige?.fortschritt(anteil, gesendet, gesamt),
|
||
beiVerarbeitung: () => anzeige?.verarbeiten(),
|
||
});
|
||
|
||
try {
|
||
const ergebnis = lauf
|
||
? await lauf.fertig
|
||
: { ok: false, netz: true };
|
||
anzeige?.weg();
|
||
|
||
if (ergebnis.abgebrochen) {
|
||
stand2.textContent = 'Abgebrochen. Es wurde nichts bereitgestellt.';
|
||
return;
|
||
}
|
||
if (!ergebnis.ok) {
|
||
stand2.textContent = ergebnis.netz
|
||
? 'Das Netz war gerade weg. Versuch es noch einmal.'
|
||
: (window.sagWas?.(ergebnis.daten?.fehler, 'Ging nicht.') || 'Ging nicht.');
|
||
return;
|
||
}
|
||
stand2.textContent = 'Steht jetzt bereit.';
|
||
feld.value = '';
|
||
const t = $('m-text'); if (t) t.value = '';
|
||
if (abFeld) abFeld.value = '';
|
||
if (bisFeld) bisFeld.value = '';
|
||
await laden();
|
||
} finally {
|
||
knopf.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ===================================================================
|
||
LADEN
|
||
=================================================================== */
|
||
/* ===================================================================
|
||
ZEICHNEN — getrennt vom Holen (22.09.2026)
|
||
-------------------------------------------------------------------
|
||
Bis heute war beides eine Funktion. Mit den Kategorien geht das
|
||
nicht mehr: Ein Klick auf „Abgelaufen" darf nicht die ganze Liste
|
||
neu vom Server holen -- das wäre ein Abruf für eine Frage, deren
|
||
Antwort schon im Browser liegt, und am Handy im Funkloch ein
|
||
Zustand, in dem man nichts sieht.
|
||
=================================================================== */
|
||
/* WARUM OBEN NICHTS STEHT, HAT DREI GRÜNDE — und der dritte fehlte.
|
||
Wer „Schon benutzt" antippt, bekommt oben immer eine leere Liste,
|
||
weil benutzte Stücke unten stehen. „Ist gerade nichts" wäre dort
|
||
schlicht gelogen und schickte jemanden los, der schon am Ziel ist. */
|
||
function leerSatz(untenAnzahl) {
|
||
if (katFilter && untenAnzahl) {
|
||
return `Unter „${ZUSTAND_WORT[katFilter] || katFilter}“ steht nichts zum `
|
||
+ 'Mitnehmen — was dazu passt, findest du weiter unten.';
|
||
}
|
||
if (katFilter) {
|
||
return `In „${ZUSTAND_WORT[katFilter] || katFilter}“ ist gerade nichts. `
|
||
+ 'Tipp oben auf „Alle“, um wieder alles zu sehen.';
|
||
}
|
||
return stand.darf_einstellen
|
||
? 'Gerade nichts frei. Stell oben etwas bereit — dann können die anderen es sich holen.'
|
||
: 'Gerade ist nichts frei. Schau später noch einmal rein — hier kommt regelmäßig Neues dazu.';
|
||
}
|
||
|
||
function zeichnen() {
|
||
if (!stand) return;
|
||
$('neu-block').hidden = !stand.darf_einstellen;
|
||
|
||
const alle = stand.material || [];
|
||
katLeisteBauen(alle);
|
||
|
||
/* DER FILTER GILT FÜR BEIDE ABSCHNITTE — sonst ist er keiner.
|
||
Im ersten Entwurf filterte nur die obere Liste. „Schon benutzt"
|
||
anzutippen ergab damit oben zwangsläufig nichts (ein benutztes
|
||
Stück steht nie zum Mitnehmen) und unten unverändert alles: Der
|
||
Knopf sah aus, als täte er nichts, und die Zahl daneben passte
|
||
zu keiner der beiden Listen. */
|
||
const passt = (m) => !katFilter || m.zustand === katFilter;
|
||
const frei = alle.filter((m) => !m.genommen && passt(m));
|
||
const weg = alle.filter((m) => m.genommen && passt(m));
|
||
|
||
const zielFrei = $('frei');
|
||
zielFrei.replaceChildren();
|
||
zielFrei.setAttribute('aria-busy', 'false');
|
||
if (!frei.length) {
|
||
/* KEIN LEERER BILDSCHIRM. Ob das gut oder schlecht ist, hängt
|
||
davon ab, wer liest -- deshalb drei Sätze statt einem. Der
|
||
dritte ist neu: Eine leere Liste nach einem Klick auf eine
|
||
Kategorie ist etwas anderes als eine leere Liste. */
|
||
zielFrei.append(el('p', 'm-leer', leerSatz(weg.length)));
|
||
}
|
||
for (const m of frei) zielFrei.append(karte(m));
|
||
|
||
/* ZWEI ZAHLEN FÜR FAST DASSELBE, DIE SICH WIDERSPRECHEN.
|
||
Auf dem ersten Bildschirmfoto stand oben „4 Stück zum
|
||
Mitnehmen" und eine Zeile darunter „Jetzt frei 2". Beide
|
||
stimmten — die eine zählt alles Freie, die andere nur das, was
|
||
heute schon gilt —, aber wer sie zum ersten Mal sieht, hält
|
||
eine davon für falsch und traut ab da keiner mehr.
|
||
|
||
Also stehen sie jetzt in EINEM Satz und erklären sich
|
||
gegenseitig. Und nur dann, wenn sie sich unterscheiden: Steht
|
||
„4, davon 4 sofort", ist das keine Auskunft, sondern Lärm. */
|
||
const sofort = frei.filter((m) => m.zustand === 'frei').length;
|
||
$('frei-satz').textContent = !frei.length ? ''
|
||
: katFilter
|
||
? `${frei.length} in „${ZUSTAND_WORT[katFilter] || katFilter}“. `
|
||
+ 'Wer es nimmt, hat es — danach ist es für alle anderen weg.'
|
||
: `${frei.length} Stück zum Mitnehmen`
|
||
+ (sofort === frei.length ? '. ' : `, davon ${sofort} sofort. `)
|
||
+ 'Wer es nimmt, hat es — danach ist es für alle anderen weg.';
|
||
|
||
$('weg-block').hidden = !weg.length;
|
||
const zielWeg = $('weg');
|
||
zielWeg.replaceChildren();
|
||
for (const m of weg) zielWeg.append(karte(m));
|
||
if (weg.length) {
|
||
$('weg-satz').textContent = stand.sieht_wer
|
||
? `${weg.length} schon vergeben — mit Namen, damit du weißt, wer was postet.`
|
||
: `${weg.length} schon vergeben. Damit nichts zweimal gepostet wird.`;
|
||
}
|
||
|
||
$('laedt').hidden = true;
|
||
$('haupt')?.removeAttribute('aria-busy');
|
||
melde('');
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const a = await fetch('/workspace/api/material');
|
||
if (!a.ok) {
|
||
window.ladefehler?.({ nochmal: laden });
|
||
return;
|
||
}
|
||
stand = await a.json();
|
||
zeichnen();
|
||
} catch {
|
||
window.ladefehler?.({ nochmal: laden });
|
||
}
|
||
}
|
||
|
||
einstellenAufbauen();
|
||
laden();
|
||
})();
|