Files
dogfather-universe/workspace/assets/js/material.js
T
DogFatherGitandClaude Opus 5 fc823240ab screen4: Dogi-Media — bearbeiten, entfernen, von wann bis wann
Filipe, 22.09.2026: "wenn wir in der kategorie material was hochladen
will ich dass die rechte und linke hand es auch bearbeiten und loeschen
koennen. dogfather auch fals es ein fehler gab. ... uebrigens ersetz das
wort material durch Dogi-Media. und mach kategorien welche sind benutzt
welche nicht welche sind noch offen, welche laufen von wan bis wann."

WAS JETZT GEHT
- Rechte Hand, linke Hand und DogFather bearbeiten und entfernen jedes
  Stueck, nicht nur ihr eigenes. Bearbeitet wird Text und Zeitfenster,
  nicht die Datei: Sonst zeigte dieselbe Nummer etwas anderes als das,
  was sich jemand gerade angesehen hat.
- Ein vergebenes Stueck bleibt unberuehrt (409). Wer es genommen hat,
  hat sich auf diesen Text verlassen.
- Vier Zustaende, gerechnet statt gespeichert: jetzt frei, kommt noch,
  abgelaufen, schon benutzt. Die Kategorieleiste zeigt zu jedem die
  Anzahl aus den echten Daten und filtert BEIDE Listen.
- "Material" heisst ueberall "Dogi-Media", der Dateiname bleibt.

DREI FEHLER AUS DEM EIGENEN ENTWURF, ALLE VOR DEM AUSLIEFERN GEMESSEN

1. `tagLokal()` ohne Argument gab "NaN-NaN-NaN" zurueck -- eine
   Zeichenkette, die aussieht wie ein Datum. Im Vergleich gewinnt das N
   gegen jede Ziffer, also galt JEDES Stueck mit Enddatum vom ersten Tag
   an als abgelaufen, und "Kommt noch" gab es nie. Kein Absturz, keine
   Meldung. Die drei Kopien der Funktion waren hier auseinandergelaufen;
   alle drei haben jetzt die Vorbelegung und werfen bei einer Zahl, die
   keine ist.
2. `frageNach` liefert bei einer reinen Rueckfrage `true`, nicht
   `{ ok: true }`. Der Entwurf prueft auf `erg?.ok` -- "Entfernen" waere
   ein Knopf gewesen, der nichts tut.
3. Drei erfundene Klassennamen (`knopf--leise`, `m-kat__knopf`,
   `m-karte__weg-knopf`) statt der vorhandenen des Hauses. Jetzt
   `.knopf-still`, `.knopf-still--warn` und die Filterleiste `.filter`.

EIN FUND IM BESTAND, ZWEI WOCHEN ALT

In start.css fehlten an einer Stelle die zwei Zeichen, die einen
Kommentar schliessen. CSS-Kommentare schachteln nicht: Der Block lief
bis zum naechsten Abschluss weiter und verschluckte `.kacheln {`; die
Fehlerbehebung des Browsers nahm danach auch die Regel `.kachel` mit.
In Chromium gemessen, beide Fassungen nacheinander: 738 statt 740
Regeln.

Der Entwurf, der dadurch nie gewirkt hat, ist NICHT wiederhergestellt
worden: Seine Flaeche mischte den Kachelton mit 30/11/3 Prozent ein --
pruef-kachelfarben fiel sofort von 36,5 % auf 4,1 % angekommene Farbe,
und 31 Toene sahen gleich aus. Das ist das Gegenteil dessen, was am
22.09. verlangt war. Er ist geloescht, mit dem Grund daneben. Offen
bleibt eine Frage an Filipe, keine Entscheidung von mir: zu demselben
Entwurf gehoerte ein schmaleres Kachelraster (fuenf bis sechs statt
drei pro Reihe). Das aendert das Aussehen der Startseite sichtbar und
bleibt deshalb, wie es ist.

GEMEINSAME BAUSTEINE AUS DEN SEITENDATEIEN GEHOLT
`.filter` stand Zeichen fuer Zeichen doppelt in dateien.css und
bereich.css, `.feld-hinweis` in aufgaben.css und leistung.css (und die
zwei waren schon auseinandergelaufen). Dogi-Media war jeweils die
dritte Seite, die sie braucht, und laedt keine davon. Derselbe Fehler
wie damals bei .knopf-still und beim Schalter -- jetzt in gate.css
bzw. start.css.

GEPRUEFT
pruef-material 155/0 (von 126, neu: Zeitfenster, Bearbeiten, Entfernen,
Rechte in der Liste und ein Abschnitt am echten Bildschirm mit Browser).
pruef-css-klassen um eine Pruefung erweitert, die genau den
start.css-Fehler findet -- mit sieben Gegenproben, darunter der echte
Fall. pruef-kachelfarben 22/0, pruef-start-ansicht, pruef-struktur,
pruef-meldungen, pruef-zeitraum, pruef-content, pruef-serien 69/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 13:59:55 +02:00

621 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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();
})();