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]>
This commit is contained in:
2026-09-22 13:59:55 +02:00
co-authored by Claude Opus 5
parent fe1dad48e5
commit fc823240ab
54 changed files with 2287 additions and 668 deletions
+182 -7
View File
@@ -62,6 +62,11 @@ import {
db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER,
istDogFather, TEAM_DOGI_ROLLEN, TREFF_ROLLEN,
} from "./workspace.js";
/* DIE UHR DES HAUSES, nicht `new Date().toISOString().slice(0,10)`.
Das waere UTC -- zwischen Mitternacht und zwei Uhr laege der Tag
damit einen zurueck, und ein Stueck, das heute anfaengt, staende
noch als "spaeter" da. */
import { tagLokal } from "./helfer-zeit.mjs";
export const materialRouter = express.Router();
@@ -103,9 +108,69 @@ function tabellen() {
CREATE INDEX IF NOT EXISTS idx_material_frei
ON material (genommen_von, erstellt);
`);
/* VON WANN BIS WANN (22.09.2026).
===================================================================
Filipe: "welche sind benutzt welche nicht welche sind noch offen,
welche laufen von wan bis wann, das muss man auch machen koennen."
Ein Bild zur Halloween-Aktion ist am 2. November kein Angebot mehr,
sondern ein Stolperstein. Ohne Fenster bleibt es bis in alle
Ewigkeit in der Liste, und irgendwann postet es jemand.
BEIDE FELDER SIND FREIWILLIG. Der haeufigste Fall ist "gilt
einfach" -- ein Pflichtfeld erzwaenge hier ein erfundenes Datum,
und ein erfundenes Datum ist schlechter als keins.
ERGAENZT STATT ANGENOMMEN: Die Tabelle steht auf echten Rechnern
schon mit Daten. `ALTER TABLE` waere beim zweiten Start ein
Fehler, ein neues CREATE wuerde die vorhandenen Zeilen nicht
erreichen. Also nachsehen, was da ist, und nur ergaenzen, was
fehlt -- dieselbe Bauart wie bei den Personen-Spalten. */
const da = new Set(d.prepare("PRAGMA table_info(material)").all().map((c) => c.name));
for (const [spalte, form] of [["gilt_ab", "TEXT"], ["gilt_bis", "TEXT"]]) {
if (da.has(spalte)) continue;
try {
d.exec(`ALTER TABLE material ADD COLUMN ${spalte} ${form}`);
console.log(`[material] Spalte '${spalte}' ergaenzt.`);
} catch (f) { console.error(`[material] Spalte '${spalte}':`, f?.message); }
}
bereit = true;
}
/* ---------------------------------------------------------------------
IN WELCHEM ZUSTAND IST EIN STUECK?
---------------------------------------------------------------------
Filipe will die Liste nach Zustand sortieren koennen. Der Zustand
wird GERECHNET und nicht gespeichert: Eine Spalte "zustand" muesste
jemand taeglich nachfuehren, und am Tag, an dem das vergessen wird,
steht "laeuft" an etwas, das seit drei Wochen abgelaufen ist.
Vier Zustaende, und die Reihenfolge der Fragen entscheidet:
benutzt -- jemand hat es genommen. Schlaegt alles andere; ein
vergebenes Stueck ist weg, auch wenn sein Fenster
noch laeuft.
spaeter -- das Fenster faengt erst an.
abgelaufen -- das Fenster ist vorbei.
frei -- alles andere. Auch alles ohne Fenster: "kein Datum"
heisst "gilt", nicht "gilt nicht".
--------------------------------------------------------------------- */
export function zustandVon(z, heute) {
if (z.genommen_von) return "benutzt";
if (z.gilt_ab && z.gilt_ab > heute) return "spaeter";
if (z.gilt_bis && z.gilt_bis < heute) return "abgelaufen";
return "frei";
}
/** Ein Datum im Format JJJJ-MM-TT -- oder null. Nichts dazwischen. */
function datumOderNull(roh) {
const t = String(roh ?? "").trim();
if (!t) return null;
if (!/^\d{4}-\d{2}-\d{2}$/.test(t)) return undefined; /* undefined = ungueltig */
const d = new Date(`${t}T12:00:00Z`);
return Number.isNaN(d.getTime()) ? undefined : t;
}
/* ---------------------------------------------------------------------
WER DARF WAS
---------------------------------------------------------------------
@@ -127,6 +192,24 @@ export const darfMaterialNehmen = (person) =>
export const siehtWerGenommenHat = (person) =>
!!person && (istDogFather(person) || person.rolle === "hand");
/* WER FREMDES BEARBEITEN UND ENTFERNEN DARF (22.09.2026).
---------------------------------------------------------------------
Filipe: "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."
Bis heute kam nur der Einsteller selbst und DogFather an ein Stueck.
Wer einen Tippfehler im Text hatte oder das falsche Bild erwischte,
musste warten, bis jemand anders Zeit hat -- und in der Zwischenzeit
steht das Falsche da, wo die Community sich bedient.
NICHT ueber `darfMaterialEinstellen`: Dort stehen auch die Modis
drin, und die sollen ihr eigenes Stueck zuruecknehmen koennen, aber
nicht das von jemand anderem. Wer fremdes anfasst, ist die Leitung. */
export const darfFremdesVerwalten = (person) =>
!!person && (istDogFather(person)
|| person.rolle === "hand" || person.rolle === "linke");
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
@@ -148,7 +231,7 @@ materialRouter.get("/workspace/api/material", (req, res) => {
const zeigtNamen = siehtWerGenommenHat(req.person);
const reihen = db().prepare(`
SELECT m.id, m.dateiname, m.art, m.groesse, m.text, m.erstellt,
m.genommen_am, m.genommen_von,
m.genommen_am, m.genommen_von, m.von_id, m.gilt_ab, m.gilt_bis,
v.name AS von_name,
g.name AS genommen_name
FROM material m
@@ -157,9 +240,17 @@ materialRouter.get("/workspace/api/material", (req, res) => {
ORDER BY (m.genommen_von IS NOT NULL), m.erstellt DESC
`).all();
/* HEUTE WIRD EINMAL GERECHNET, NICHT JE ZEILE. Sonst koennte eine
Liste, die ueber Mitternacht gelesen wird, zwei verschiedene
Tage benutzen -- und ein Stueck waere in derselben Antwort
gleichzeitig "laeuft" und "abgelaufen". */
const heute = tagLokal(0);
res.json({
darf_einstellen: darfMaterialEinstellen(req.person),
darf_verwalten: darfFremdesVerwalten(req.person),
sieht_wer: zeigtNamen,
heute,
max_bytes: MAX_BYTES,
material: reihen.map((z) => ({
id: z.id,
@@ -180,6 +271,14 @@ materialRouter.get("/workspace/api/material", (req, res) => {
/* Und jeder sieht, ob ER es war -- sonst wuesste er nicht mehr,
was er selbst schon geholt hat. */
von_mir_genommen: z.genommen_von === req.person.id,
gilt_ab: z.gilt_ab || null,
gilt_bis: z.gilt_bis || null,
zustand: zustandVon(z, heute),
/* DARF ICH DIESES STUECK ANFASSEN? Die Antwort steht beim
Stueck und wird nicht in der Oberflaeche nachgerechnet --
zwei Rechnungen fuer dieselbe Frage laufen auseinander, und
die im Browser ist die, der man nicht trauen darf. */
darf_ich: z.von_id === req.person.id || darfFremdesVerwalten(req.person),
})),
});
} catch (f) {
@@ -229,11 +328,30 @@ materialRouter.post(
const datei = `${Date.now().toString(36)}-${randomBytes(8).toString("hex")}${endung}`;
writeFileSync(join(ORDNER, datei), req.body, { flag: "wx" });
/* DAS FENSTER KOMMT WIE NAME UND TEXT IN KOPFZEILEN.
Derselbe Weg wie alles andere an diesem Aufruf -- die Datei
liegt roh im Rumpf, also ist fuer Angaben nur der Kopf uebrig.
Ein zweiter Aufruf danach waere ein zweiter Weg, der beim
Abbrechen ein halbes Stueck hinterliesse. */
const ab = datumOderNull(req.get("x-gilt-ab"));
const bis = datumOderNull(req.get("x-gilt-bis"));
if (ab === undefined || bis === undefined) {
return res.status(400).json({ fehler: "datum_unbrauchbar" });
}
/* EIN FENSTER, DAS RUECKWAERTS LAEUFT, IST KEINS. Es waere in
jedem Zustand gleichzeitig "spaeter" und "abgelaufen" -- und
die Liste zeigte dann, was die Reihenfolge der Fragen
zufaellig ergibt, statt was jemand gemeint hat. */
if (ab && bis && ab > bis) {
return res.status(400).json({ fehler: "fenster_verkehrt" });
}
const { lastInsertRowid } = db().prepare(`
INSERT INTO material (datei, dateiname, typ, art, groesse, text, von_id, erstellt)
VALUES (?,?,?,?,?,?,?,?)
INSERT INTO material (datei, dateiname, typ, art, groesse, text, von_id, erstellt,
gilt_ab, gilt_bis)
VALUES (?,?,?,?,?,?,?,?,?,?)
`).run(datei, name, typ, art, req.body.length, text, req.person.id,
new Date().toISOString());
new Date().toISOString(), ab, bis);
protokolliere(req.person.id, "material_eingestellt",
`${art} "${name}" (${req.body.length} Bytes)`, echteIp(req));
@@ -277,7 +395,7 @@ materialRouter.post("/workspace/api/material/:id/nehmen", (req, res) => {
});
}
protokolliere(req.person.id, "material_genommen", `Material ${id}`, echteIp(req));
protokolliere(req.person.id, "material_genommen", `Dogi-Media ${id}`, echteIp(req));
res.json({ ok: true, weg: `/workspace/api/material/${id}/datei` });
} catch (f) {
console.error("[material] Nehmen:", f?.message);
@@ -362,17 +480,74 @@ materialRouter.get("/workspace/api/material/:id/datei", (req, res) => {
/* ---------------------------------------------------------------------
WEGNEHMEN
--------------------------------------------------------------------- */
/* ---------------------------------------------------------------------
BEARBEITEN (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."
GEAENDERT WERDEN TEXT UND FENSTER -- die DATEI nicht. Wer ein
anderes Bild will, stellt ein anderes Stueck ein: Sonst zeigte die
Liste nach der Aenderung etwas anderes als das, was jemand sich
gerade angesehen hat, unter derselben Nummer.
EIN VERGEBENES STUECK BLEIBT, WIE ES IST. Wer es genommen hat, hat
sich auf diesen Text und dieses Fenster verlassen; es hinterher zu
aendern hiesse, die Absprache nachtraeglich umzuschreiben.
--------------------------------------------------------------------- */
materialRouter.patch("/workspace/api/material/:id", express.json({ limit: "32kb" }),
(req, res) => {
try {
const id = Number(req.params.id);
const z = db().prepare("SELECT * FROM material WHERE id = ?").get(id);
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
if (z.von_id !== req.person.id && !darfFremdesVerwalten(req.person)) {
return res.status(403).json({ fehler: "nicht_deins" });
}
/* EIGENER CODE, NICHT `schon_vergeben`: Der ist im Haus schon
vergeben -- fuer Aufgaben ("die Aufgabe ist schon uebernommen").
Ein Code, der zwei Dinge bedeutet, zeigt frueher oder spaeter
an einer der beiden Stellen den falschen Satz an. */
if (z.genommen_von) return res.status(409).json({ fehler: "schon_im_umlauf" });
const text = String(req.body?.text ?? z.text ?? "").trim().slice(0, 600);
const ab = req.body?.gilt_ab === undefined
? z.gilt_ab : datumOderNull(req.body.gilt_ab);
const bis = req.body?.gilt_bis === undefined
? z.gilt_bis : datumOderNull(req.body.gilt_bis);
if (ab === undefined || bis === undefined) {
return res.status(400).json({ fehler: "datum_unbrauchbar" });
}
if (ab && bis && ab > bis) {
return res.status(400).json({ fehler: "fenster_verkehrt" });
}
db().prepare("UPDATE material SET text = ?, gilt_ab = ?, gilt_bis = ? WHERE id = ?")
.run(text, ab, bis, id);
protokolliere(req.person.id, "material_geaendert", `Dogi-Media ${id}`, echteIp(req));
res.json({ ok: true, id, text, gilt_ab: ab, gilt_bis: bis,
zustand: zustandVon({ ...z, gilt_ab: ab, gilt_bis: bis }, tagLokal(0)) });
} catch (f) {
console.error("[material] Aendern:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
materialRouter.delete("/workspace/api/material/:id", (req, res) => {
try {
const id = Number(req.params.id);
const z = db().prepare("SELECT * FROM material WHERE id = ?").get(id);
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
if (z.von_id !== req.person.id && !istDogFather(req.person)) {
/* ERWEITERT AM 22.09.2026: auch rechte und linke Hand.
Vorher kam nur der Einsteller und DogFather an ein Stueck. Wer
das falsche Bild erwischt hatte, musste warten -- und bis dahin
stand das Falsche dort, wo die Community sich bedient. */
if (z.von_id !== req.person.id && !darfFremdesVerwalten(req.person)) {
return res.status(403).json({ fehler: "nicht_deins" });
}
db().prepare("DELETE FROM material WHERE id = ?").run(id);
try { unlinkSync(join(ORDNER, z.datei)); } catch { /* schon weg */ }
protokolliere(req.person.id, "material_entfernt", `Material ${id}`, echteIp(req));
protokolliere(req.person.id, "material_entfernt", `Dogi-Media ${id}`, echteIp(req));
res.json({ ok: true });
} catch (f) {
console.error("[material] Entfernen:", f?.message);