GEFUNDEN UM 01:22, von pruef-kreislauf -- und nur, weil nachts
gearbeitet wurde.
Die Pruefung legt einen Wunsch mit dem HEUTIGEN Datum an und macht
daraus einen Termin. Gemessen:
geschickt: datum = 2026-10-01 (heuteLokal auf dem Server)
gespeichert: datum = 2026-09-30
Der Termin lag also in der Vergangenheit. Auf „Was ansteht" sortiert
er sich damit in den Abschnitt „Vorbei" ein -- und der ist mit
Absicht zugeklappt. Beim Wunsch stand „Daraus wurde ein Termin", auf
dem Brett war er nicht zu sehen. Zwei Stunden jede Nacht (im Winter
eine), genau in den Stunden, in denen hier gearbeitet wird.
URSACHE
const jetzt = () => new Date().toISOString(); // UTC
... jetzt().slice(0, 10) ... // UTC-Tag
ES WAR NICHT EINE STELLE. Nachgemessen: SECHZEHN in vierzehn Modulen,
davon ZEHN, die den falschen Tag in die Datenbank schreiben --
Termine, Wuensche, Highlights, Talente, Leads, Videos, Vorlagen --
und sechs, die „heute" vergleichen (ueberfaellige Aufgaben, Berichte,
die Frist einer Entwicklungsaufgabe).
NICHT ANGEFASST, WEIL RICHTIG: Rechnungen auf einem Datumstext mit
fester Uhrzeit (`Date.parse(tag + "T12:00:00Z") + n * 86400000`). Die
bleiben in jeder Zeitzone am selben Kalendertag -- kalender, teamlage
und serien machen es so, und das bleibt.
WARUM ES NIEMAND GEMERKT HAT
pruef-struktur sucht dieses Muster seit dem 06.09.2026. Aber:
- sie sah NUR in die `pruef-*.mjs`, nie in die Anwendung
- sie kannte die Schreibweise ueber eine FUNKTION nicht
(`const jetzt = () => ...` statt `const jetzt = ...`)
Die Wache stand vor den Pruefungen, nicht vor dem Haus -- derselbe
Fehler wie heute Nacht bei den Messports: eine Sicherung, die nur die
halbe Menge kennt, faellt in der anderen Haelfte aus, und zwar
lautlos, denn sie meldet ja „nichts gefunden".
Jetzt sieht sie in beides und kennt beide Schreibweisen. Beim ersten
scharfen Lauf fand sie sofort 23 weitere Stellen in den Pruefdateien
selbst -- dieselben Zeitbomben, gegen die sie gebaut worden war.
EINE ZWEITE WACHE, WEIL ICH SELBST HINEINGELAUFEN BIN
Mein Umbauwerkzeug hat in elf Modulen `heuteLokal()` eingesetzt und
die Einfuhr weggelassen: Es hat erst ersetzt und DANN gefragt, ob der
Name schon in der Datei steht -- da stand er, mein eigener Aufruf.
`node --check` sagt dazu nichts, „Laedt jedes Server-Modul?" auch
nicht: Die Datei ist syntaktisch tadellos. Erst der Aufruf faellt um
mit `ReferenceError: heuteLokal is not defined`. Gefunden hat es
pruef-video, zufaellig. Die anderen zehn waeren durchgerutscht.
Deshalb neu: „Ruft ein Modul etwas, das es nie eingefuehrt hat?" --
die Namen des Hauses aus den export-Zeilen gelesen, nicht
aufgezaehlt. 460 Aufrufe in 350 Dateien, alle mit Einfuhr.
pruef-kreislauf STELLT JETZT DIE RICHTIGE FRAGE
Sie war rot und hat den Fehler dabei nur gestreift: „`.kette` wird
nicht sichtbar", Zeitsperre nach 15 s. Das klingt nach der Anzeige
und schickt einen zur falschen Stelle. Neu:
- eine Zeile fragt das DATUM (ohne Browser, nennt den Fehler beim
Namen)
- der Browserteil klappt zu, was zu ist, und misst dann die Kette;
„gar nicht da" wird von „da und unsichtbar" unterschieden
NEBENBEFUND IN pruef-ics
Die Probe „fast richtig" war `echt.slice(0, -1) + "A"`. Der
Schluessel ist base64url; sein letztes Zeichen ist eines von
sechzehn. Endet er auf „A", IST die Probe der echte Schluessel, der
Server antwortet zu Recht mit 200, und die Pruefung meldet ein Loch,
das es nicht gibt -- einmal je sechzehn Laeufe. Heute Nacht zweimal
hintereinander, und die Suche ging eine halbe Stunde in eine
Aenderung, die damit nichts zu tun hatte.
GEPRUEFT
pruef-struktur 44 -> 59 Pruefungen, 0 Fehler
pruef-ics 37 -> 38, 0 Fehler
pruef-kreislauf Absturz bei Nr. 17 -> 25 Pruefungen, 0 Fehler
und gruen geblieben: treff 85, arten 28, video 74, uebernahme 39,
entwicklung 79, content 45, vorlagen 24, zuteilung 75,
scout-zuteilung 37, unterstuetzung 70, aufbewahrung 45,
bewerbung 91, treff-start 42, uebergang 65, nachwuchs 262,
auskunft 46, modi-ideen 30, neue-seiten 109, spicy 85,
wege-nach-draussen 67, aufgabenbrett 49, agentur 62,
bereiche-lesend 37 -- beide Haeuser
GEGENPROBEN, DIE WIRKLICH ROT WERDEN
- den UTC-Tag im `daraus`-Weg wieder eingebaut: pruef-kreislauf
meldet „er liegt HEUTE, nicht gestern (2026-09-30, heute ist
2026-10-01)", 25 Pruefungen, 1 Fehler -- und der Browserteil
bleibt gruen, weil er jetzt aufklappt. Jede Frage bei ihrer
eigenen Pruefung.
- eine Einfuhr aus workspace-video.js entfernt: die neue Wache
meldet „workspace-video.js: heuteLokal() (aus helfer-tag.mjs)"
- beide Erkennungen je gegen einen gebauten Rueckschritt geprueft
(Funktion, Variable, zwei Schritte, Date.now-Rechnung) und gegen
das, was NICHT anschlagen darf (UTC-Mittag, voller Zeitstempel,
fremdes Date, Name im Kommentar, Eigenschaft am Objekt)
EIN FEHLER BEIM UMBAU, HIER FESTGEHALTEN: Mein erster Lauf ueber die
Pruefdateien hat stumpf ersetzt und dabei KOMMENTARE umgeschrieben --
in sieben Dateien stand die alte Schreibweise als Beleg in der
Begruendung, und daraus wurde das Gegenteil. Bemerkt hat es der
Vergleich der Zahlen (34 Stellen statt der gemessenen 24), nicht die
Absicht. Zurueckgenommen und mit Schutz fuer Kommentare und
Zeichenketten wiederholt.
Datenbank vorher gesichert. Keine Schemaaenderung.
Co-Authored-By: Claude Opus 5 <[email protected]>
371 lines
16 KiB
JavaScript
371 lines
16 KiB
JavaScript
/* =====================================================================
|
||
UNTERSTÜTZEN — die Wege (24.09.2026)
|
||
|
||
Filipe: „jeder soll diese kachel sehen aber nur dogfather soll sie
|
||
verändern können oder vieles mehr sehen. alle anderen, rechte und
|
||
linke hand, modis und community sollen supporten können."
|
||
|
||
Warum die Wege in der Datenbank stehen und warum die Zählung keine
|
||
Personenspalte hat, steht im Kopf von unterstuetzung-tabellen.js.
|
||
Hier sind die Wege — und die vier Stellen, an denen etwas
|
||
schiefgehen kann:
|
||
|
||
1. EINE ADRESSE IST EIN VERSPRECHEN. Auf dieser Seite steht ein
|
||
Zahlungslink. Wer ihn ändern darf, kann Geld umleiten — deshalb
|
||
hängt das Schreiben an `istDogFather` und nicht an `istLeitung`.
|
||
Die rechte Hand führt dieses Team mit, und sie soll trotzdem
|
||
nicht an diesen Link können. Das ist kein Misstrauen, sondern
|
||
dieselbe Regel wie bei der Rechtetafel: Genau eine Tür bleibt
|
||
bei ihm.
|
||
|
||
2. UND SIE WIRD GEPRÜFT, BEVOR SIE IN DER DATENBANK STEHT. Erlaubt
|
||
sind `https://` und eine Seite dieses Hauses. `javascript:` wäre
|
||
ein Skript, das jeder Besucher ausführt, `http://` ein
|
||
Zahlungsweg ohne Verschlüsselung, `data:` eine ganze Seite im
|
||
Link. Geprüft wird beim SCHREIBEN: Was erst beim Anzeigen
|
||
auffällt, steht vorher schon in der Datenbank.
|
||
|
||
3. DER LESEWEG IST FÜR ALLE UND ZEIGT NICHT ALLES. Unsichtbare Wege
|
||
und die Zählung gehen nur an DogFather. Beides entscheidet EINE
|
||
Stelle (`darfAlles`), damit nicht zwei Antworten auf dieselbe
|
||
Frage entstehen.
|
||
|
||
4. EIN STRICH IST KEIN PROTOKOLL. Gezählt wird je Weg und Tag, ohne
|
||
Person. Dass derselbe Mensch nicht zehnmal zählt, merkt sich der
|
||
Server NUR im Arbeitsspeicher — nach einem Neustart ist es
|
||
vergessen, und auf der Platte steht es nie.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import {
|
||
db, protokolliere, echteIp, istDogFather, heuteLokal,
|
||
tagLokal,
|
||
} from "./workspace.js";
|
||
import { UNTERSTUETZUNG_ARTEN, ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js";
|
||
|
||
export const unterstuetzungRouter = express.Router();
|
||
|
||
const TITEL_MAX = 60;
|
||
const TEXT_MAX = 400;
|
||
const KNOPF_MAX = 40;
|
||
const ZIEL_MAX = 500;
|
||
const WEGE_MAX = 12;
|
||
|
||
/** Wer ändern darf UND wer mehr sieht — dieselbe Frage, eine Antwort.
|
||
*
|
||
* Zwei getrennte Prüfungen wären zwei Gelegenheiten, eine davon zu
|
||
* lockern. Und die beiden gehören ohnehin zusammen: Wer einen Weg
|
||
* bearbeitet, muss ihn auch sehen, solange er noch unsichtbar ist. */
|
||
const darfAlles = (person) => istDogFather(person);
|
||
|
||
/* Gleiche Herkunft — dasselbe Muster wie überall im Haus. Ohne
|
||
Origin-Kopf (Server zu Server, Prüfungen) geht es durch; mit einem
|
||
fremden nicht. */
|
||
function gleicheHerkunft(req, res, next) {
|
||
const herkunft = req.get("origin");
|
||
if (!herkunft) return next();
|
||
try {
|
||
if (new URL(herkunft).host === req.get("host")) return next();
|
||
} catch { /* unlesbar -> abweisen */ }
|
||
return res.status(403).json({ fehler: "fremde_herkunft" });
|
||
}
|
||
|
||
/** Nur DogFather. 404 und nicht 403: Wer nicht ändern darf, muss nicht
|
||
* erfahren, dass es die Möglichkeit gibt — dasselbe Vorgehen wie bei
|
||
* der Aufbewahrung und den Kanälen. */
|
||
function nurDogFather(req, res, next) {
|
||
if (!darfAlles(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
next();
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE ADRESSE
|
||
=====================================================================
|
||
|
||
Drei Formen sind erlaubt, alles andere nicht:
|
||
|
||
"" kein Ziel (ein Weg ohne Knopf)
|
||
https://… nach draußen, verschlüsselt
|
||
seite.html[?frage] eine Seite dieses Hauses
|
||
|
||
AUSDRÜCKLICH NICHT: `javascript:` (ein Skript, das jeder Besucher
|
||
ausführt), `data:` (eine ganze Seite im Link), `http://` (ein
|
||
Zahlungsweg im Klartext) und alles mit `//` am Anfang (das übernimmt
|
||
das Schema der Seite und sieht aus wie ein eigener Pfad).
|
||
|
||
Geprüft wird mit `new URL()` und nicht mit einem Muster: Ein
|
||
selbstgeschriebener Ausdruck übersieht `HtTpS:` oder ein
|
||
eingeschobenes Leerzeichen, der Parser des Browsers nicht. */
|
||
const HAUS_SEITE = /^[a-z0-9][a-z0-9-]{0,40}\.html(\?[A-Za-z0-9=&_%.-]{0,80})?$/;
|
||
|
||
function zielPruefen(roh) {
|
||
const z = String(roh ?? "").trim();
|
||
if (!z) return { ok: true, wert: "" };
|
||
if (z.length > ZIEL_MAX) return { ok: false, grund: "Die Adresse ist zu lang." };
|
||
if (HAUS_SEITE.test(z)) return { ok: true, wert: z };
|
||
let u;
|
||
try { u = new URL(z); } catch {
|
||
return { ok: false, grund: "Das ist keine gültige Adresse. Sie muss mit https:// anfangen." };
|
||
}
|
||
if (u.protocol !== "https:") {
|
||
return { ok: false, grund: "Nur https:// – ein Zahlungsweg ohne Verschlüsselung kommt hier nicht hin." };
|
||
}
|
||
return { ok: true, wert: u.href };
|
||
}
|
||
|
||
const text = (roh, max) => String(roh ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
||
|
||
/* =====================================================================
|
||
LESEN — für alle
|
||
===================================================================== */
|
||
|
||
unterstuetzungRouter.get("/workspace/api/unterstuetzung", (req, res) => {
|
||
try {
|
||
const alles = darfAlles(req.person);
|
||
const d = db();
|
||
const wege = d.prepare(
|
||
`SELECT id, schluessel, art, titel, text, knopf, ziel, sichtbar, platz
|
||
FROM unterstuetzung_wege
|
||
${alles ? "" : "WHERE sichtbar = 1"}
|
||
ORDER BY platz, id`).all();
|
||
|
||
/* WAS DOGFATHER MEHR SIEHT (und sonst niemand):
|
||
die unsichtbaren Wege — und wie oft geöffnet wurde.
|
||
|
||
DREI ZEITRÄUME, weil eine einzelne Zahl nichts sagt: „48" kann
|
||
„läuft gut" oder „lief mal" heißen. Mit 7 und 30 Tagen daneben
|
||
sieht man den Unterschied. */
|
||
let zahlen = null;
|
||
if (alles) {
|
||
const tagVor = (n) => {
|
||
const t = new Date(); t.setHours(12, 0, 0, 0); t.setDate(t.getDate() - n);
|
||
return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}`
|
||
+ `-${String(t.getDate()).padStart(2, "0")}`;
|
||
};
|
||
const roh = d.prepare(
|
||
`SELECT weg_id,
|
||
SUM(anzahl) AS gesamt,
|
||
SUM(CASE WHEN tag >= ? THEN anzahl ELSE 0 END) AS tage30,
|
||
SUM(CASE WHEN tag >= ? THEN anzahl ELSE 0 END) AS tage7
|
||
FROM unterstuetzung_striche
|
||
GROUP BY weg_id`).all(tagVor(29), tagVor(6));
|
||
zahlen = Object.fromEntries(roh.map((z) => [z.weg_id, {
|
||
gesamt: z.gesamt || 0, tage30: z.tage30 || 0, tage7: z.tage7 || 0,
|
||
}]));
|
||
}
|
||
|
||
res.json({
|
||
wege: wege.map((w) => ({ ...w, sichtbar: !!w.sichtbar })),
|
||
arten: UNTERSTUETZUNG_ARTEN,
|
||
darf_aendern: alles,
|
||
zahlen,
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[unterstuetzung] Lesen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
EINEN STRICH MACHEN — für alle
|
||
=====================================================================
|
||
|
||
Wer einen Weg öffnet, macht einen Strich. Kein Name, keine Nummer,
|
||
keine Kennung: nur Weg und Tag.
|
||
|
||
DIE SPERRE GEGEN MEHRFACHZÄHLUNG LIEGT IM ARBEITSSPEICHER. Sie
|
||
müsste sonst eine Kennung je Mensch auf die Platte schreiben — und
|
||
damit wäre die Zusage dieser Seite gebrochen, für eine Zahl, die nur
|
||
ungefähr stimmen muss. Nach einem Neustart zählt jeder wieder einmal;
|
||
das ist der Preis, und er ist richtig herum bezahlt. */
|
||
const heuteSchon = new Map(); // "sitzung|weg" -> Tag
|
||
unterstuetzungRouter.post("/workspace/api/unterstuetzung/:id(\\d+)/strich",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const weg = db().prepare(
|
||
"SELECT id, sichtbar FROM unterstuetzung_wege WHERE id = ?").get(id);
|
||
/* Ein unsichtbarer Weg wird nicht gezählt. Sonst stünde eine Zahl
|
||
an einem Weg, den außer DogFather niemand sehen kann — und die
|
||
käme dann von ihm selbst. */
|
||
if (!weg || (!weg.sichtbar && !darfAlles(req.person))) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
|
||
const tag = heuteLokal();
|
||
const schluessel = `${req.person?.id ?? "?"}|${id}`;
|
||
if (heuteSchon.get(schluessel) === tag) return res.json({ gezaehlt: false });
|
||
heuteSchon.set(schluessel, tag);
|
||
/* Der Speicher wächst sonst mit jedem Menschen und jedem Weg. Bei
|
||
mehr als tausend Einträgen fängt er von vorn an — eine Zahl,
|
||
die nur ungefähr stimmen muss, verträgt das. */
|
||
if (heuteSchon.size > 1000) heuteSchon.clear();
|
||
|
||
db().prepare(
|
||
`INSERT INTO unterstuetzung_striche (weg_id, tag, anzahl) VALUES (?,?,1)
|
||
ON CONFLICT(weg_id, tag) DO UPDATE SET anzahl = anzahl + 1`).run(id, tag);
|
||
res.json({ gezaehlt: true });
|
||
} catch (fehler) {
|
||
console.error("[unterstuetzung] Strich:", fehler?.message);
|
||
/* Ein Zähler darf den Weg nach draußen nie blockieren: Der Browser
|
||
öffnet den Link ohnehin, egal was hier steht. */
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
ÄNDERN — nur DogFather
|
||
===================================================================== */
|
||
|
||
unterstuetzungRouter.put("/workspace/api/unterstuetzung/:id(\\d+)",
|
||
gleicheHerkunft, nurDogFather, express.json({ limit: "8kb" }), (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const alt = db().prepare("SELECT * FROM unterstuetzung_wege WHERE id = ?").get(id);
|
||
if (!alt) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const titel = text(req.body?.titel ?? alt.titel, TITEL_MAX);
|
||
if (titel.length < 2) return res.status(400).json({ fehler: "Der Weg braucht einen Namen." });
|
||
const art = String(req.body?.art ?? alt.art);
|
||
if (!UNTERSTUETZUNG_ARTEN[art]) return res.status(400).json({ fehler: "Diese Art gibt es nicht." });
|
||
|
||
const z = zielPruefen(req.body?.ziel ?? alt.ziel);
|
||
if (!z.ok) return res.status(400).json({ fehler: z.grund });
|
||
|
||
const knopf = text(req.body?.knopf ?? alt.knopf, KNOPF_MAX);
|
||
/* EIN KNOPF OHNE ZIEL IST EIN VERSPRECHEN INS LEERE. Er sieht aus
|
||
wie ein Weg und tut nichts — und weil nichts passiert, hält man
|
||
die Seite für kaputt, nicht die Eingabe für unvollständig. */
|
||
if (knopf && !z.wert) {
|
||
return res.status(400).json({
|
||
fehler: "Ein Knopf braucht eine Adresse – sonst passiert beim Drücken nichts.",
|
||
});
|
||
}
|
||
/* UND EIN SICHTBARER WEG MIT ZIEL BRAUCHT EINEN KNOPF, sonst
|
||
steht die Adresse da und niemand kommt hin. */
|
||
const sichtbar = req.body?.sichtbar === undefined
|
||
? !!alt.sichtbar : !!req.body.sichtbar;
|
||
if (sichtbar && z.wert && !knopf) {
|
||
return res.status(400).json({
|
||
fehler: "Zu einer Adresse gehört eine Aufschrift für den Knopf.",
|
||
});
|
||
}
|
||
|
||
db().prepare(`UPDATE unterstuetzung_wege
|
||
SET titel = ?, text = ?, knopf = ?, ziel = ?, art = ?, sichtbar = ?,
|
||
platz = ?, geaendert = ?, geaendert_von = ?
|
||
WHERE id = ?`)
|
||
.run(titel, text(req.body?.text ?? alt.text, TEXT_MAX), knopf, z.wert, art,
|
||
sichtbar ? 1 : 0,
|
||
Number.isInteger(Number(req.body?.platz)) ? Number(req.body.platz) : alt.platz,
|
||
new Date().toISOString(), req.person.id, id);
|
||
|
||
/* PROTOKOLLIERT WIRD, DASS DIE ADRESSE SICH GEÄNDERT HAT — nicht,
|
||
wie sie lautet. Ein Zahlungslink gehört nicht in ein Protokoll,
|
||
das andere lesen können; dass er gewechselt hat, schon. */
|
||
protokolliere("unterstuetzung_geaendert", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${alt.schluessel}${alt.ziel !== z.wert ? " (Adresse gewechselt)" : ""}`,
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[unterstuetzung] Aendern:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
unterstuetzungRouter.post("/workspace/api/unterstuetzung",
|
||
gleicheHerkunft, nurDogFather, express.json({ limit: "8kb" }), (req, res) => {
|
||
try {
|
||
const d = db();
|
||
const { n } = d.prepare("SELECT COUNT(*) AS n FROM unterstuetzung_wege").get();
|
||
/* EINE OBERGRENZE, und zwar eine niedrige. Zwanzig Wege sind
|
||
keine Auswahl mehr, sondern eine Wand — und die Seite soll
|
||
leicht sein. */
|
||
if (n >= WEGE_MAX) {
|
||
return res.status(400).json({
|
||
fehler: `Mehr als ${WEGE_MAX} Wege werden unübersichtlich. Räum erst einen weg.`,
|
||
});
|
||
}
|
||
const titel = text(req.body?.titel, TITEL_MAX);
|
||
if (titel.length < 2) return res.status(400).json({ fehler: "Der Weg braucht einen Namen." });
|
||
const art = String(req.body?.art ?? "frei");
|
||
if (!UNTERSTUETZUNG_ARTEN[art]) return res.status(400).json({ fehler: "Diese Art gibt es nicht." });
|
||
const z = zielPruefen(req.body?.ziel);
|
||
if (!z.ok) return res.status(400).json({ fehler: z.grund });
|
||
const knopf = text(req.body?.knopf, KNOPF_MAX);
|
||
if (knopf && !z.wert) {
|
||
return res.status(400).json({
|
||
fehler: "Ein Knopf braucht eine Adresse – sonst passiert beim Drücken nichts.",
|
||
});
|
||
}
|
||
|
||
/* Der Schlüssel wird ABGELEITET und nicht getippt. Er hält die
|
||
Striche der letzten Monate am Weg fest, auch wenn der Titel
|
||
sich ändert — und ein „eigen-" davor kann nie mit einem der
|
||
fünf Startschlüssel zusammenfallen. */
|
||
const rein = titel.toLowerCase()
|
||
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
|
||
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30) || "weg";
|
||
let schluessel = "eigen-" + rein;
|
||
if (d.prepare("SELECT 1 FROM unterstuetzung_wege WHERE schluessel = ?").get(schluessel)) {
|
||
schluessel += "-" + Date.now().toString(36).slice(-4);
|
||
}
|
||
const { platz } = d.prepare(
|
||
"SELECT COALESCE(MAX(platz), 0) + 10 AS platz FROM unterstuetzung_wege").get();
|
||
|
||
d.prepare(`INSERT INTO unterstuetzung_wege
|
||
(schluessel, art, titel, text, knopf, ziel, sichtbar, platz, geaendert, geaendert_von)
|
||
VALUES (?,?,?,?,?,?,1,?,?,?)`)
|
||
.run(schluessel, art, titel, text(req.body?.text, TEXT_MAX), knopf, z.wert,
|
||
platz, new Date().toISOString(), req.person.id);
|
||
|
||
protokolliere("unterstuetzung_angelegt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: schluessel,
|
||
});
|
||
res.json({ id: d.prepare("SELECT last_insert_rowid() AS id").get().id });
|
||
} catch (fehler) {
|
||
console.error("[unterstuetzung] Anlegen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
unterstuetzungRouter.delete("/workspace/api/unterstuetzung/:id(\\d+)",
|
||
gleicheHerkunft, nurDogFather, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const weg = db().prepare(
|
||
"SELECT schluessel FROM unterstuetzung_wege WHERE id = ?").get(id);
|
||
if (!weg) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
/* Die Striche gehen mit (ON DELETE CASCADE). Sie ohne ihren Weg
|
||
stehen zu lassen hieße, eine Zahl aufzubewahren, zu der niemand
|
||
mehr sagen kann, wozu sie gehört. */
|
||
db().prepare("DELETE FROM unterstuetzung_wege WHERE id = ?").run(id);
|
||
protokolliere("unterstuetzung_entfernt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: weg.schluessel,
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[unterstuetzung] Entfernen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/** Alte Tagesstriche wegräumen — aus dem Rundgang, nicht beim Lesen.
|
||
*
|
||
* Hinge es am Öffnen der Seite, bliebe alles liegen, solange niemand
|
||
* hinsieht. */
|
||
export function unterstuetzungAufraeumen() {
|
||
try {
|
||
/* ORTSZEIT (01.10.2026): Die Tage in dieser Tabelle stehen in
|
||
Ortszeit, die Grenze stand in UTC. Nachts wurde dadurch ein Tag
|
||
zu wenig geraeumt -- kein Schaden, aber zwei Rechnungen fuer
|
||
denselben Begriff, und eine davon laeuft irgendwann weg. */
|
||
const grenze = tagLokal(-ZAEHLUNG_TAGE);
|
||
const r = db().prepare("DELETE FROM unterstuetzung_striche WHERE tag < ?").run(grenze);
|
||
return r.changes || 0;
|
||
} catch { return 0; }
|
||
}
|