Der Notizblock -- und eine Pruefung, die zwoelf Kacheln nie angesehen hat
Filipe: „fuer die modis, rechte und linke hand und dogfather eine
kachel hinzufuegst, sie soll: Notizen, heissen. ich will dass du das
auch wie ein notizblock erstellst. ich will dass es uebelst geil und
einzigartig ist."
=================================================================
TEIL 1: DIE FARBE -- UND WAS DABEI AUFFIEL
=================================================================
Fuer die neue Kachel braucht es einen Ton. Beim Suchen fiel auf, dass
tools/kachelton-entzerren.mjs zwoelf der 45 Farben fuer FREI hielt.
Sie sind es nicht. bereicheFuer() gibt fuer die fuenf Rollen des
Agenturhauses null zurueck -- das heisst „nimm die Liste aus dem
Browser", und die steht in workspace/assets/js/bereiche.js. Dort
stehen die Toene 1 bis 22 und 44: Dashboard, Zahlen, Team-Lage,
Scout-Pipeline. Kacheln, die jeden Tag jemand ansieht.
pruef-kachelfarben hatte denselben blinden Fleck. Sie meldete seit
Wochen „33 benutzte Toene" und war gruen. Was sie dadurch NICHT sah:
* Der Farblauf von gestern Nacht hat zwoelf dieser Kacheln
verschoben und drei davon unter die Buntheitsgrenze gedrueckt.
Gruen geblieben.
* Ton 7, 16 und 22 lagen SEIT JEHER unter der Grenze (0,112 / 0,116
/ 0,068). Nie gemeldet.
Das ist die Sorte gruener Haken, vor der die Hausregel warnt: Er sagt
nur, dass die Bedingung erfuellt war -- nicht, dass sie das Richtige
angesehen hat.
BEHOBEN, und zwar an der Wurzel: Die REGELN (Kontrast, Buntheit)
gelten jetzt fuer jeden Ton, der irgendwo an einer Kachel steht --
gelesen aus denselben fuenf Dateien, die auch pruef-kachel-universum
liest. Dazu die Namen der Kacheln, damit „Ton 1 traegt Dashboard"
ueberhaupt pruefbar ist.
DIE PALETTE WURDE NEU GERECHNET, mit dem dritten Verfahren an einem
Tag -- die ersten zwei stehen als Fehler im Kopf des Werkzeugs:
1. Alle 45 neu verteilt. Lief ueber zwei ausdrueckliche Wuensche
hinweg (#ff1a1a, #a8d8ff).
2. Nur die Kollisionen, aber immer nur EINEN der beiden bewegt.
Ergebnis: Ton 1 sprang vom Tuerkis ins Altrosa, 0,197 weit.
3. BEIDE duerfen sich bewegen, und zwar beide nur ein bisschen.
Zwei Toene, die 0,02 auseinanderstehen und 0,09 brauchen, teilen
sich das -- jeder rueckt 0,045, und beide bleiben, was sie waren.
kleinster Abstand 0,0154 -> 0,0905
zu blass 4 -> 0
sichtbar veraendert 7 Kacheln (ueber 0,05)
kaum zu sehen 28 (13 zwischen 0,02 und 0,05, 15 darunter)
EINE AUSNAHME MIT ZAHL, keine mit Achselzucken: Ton 1 (Dashboard)
bleibt acht Tausendstel unter der Buntheitsgrenze. Gemessen ueber ALLE
16,7 Millionen sRGB-Farben ist die naechste, die alle Regeln haelt,
#7a75c8 -- ein Blauviolett, 0,113 entfernt. Tuerkis erreicht in sRGB
schlicht keine hoehere Buntheit, und das schmale Band teilen sich
schon sieben Toene. Eine Kachel, die ihre Farbfamilie behaelt, ist die
bessere Antwort auf „richtig geil und speziell" als eine, die acht
Tausendstel bunter ist und niemand wiedererkennt. blassBis sagt jetzt
bei jeder Ausnahme, WIE WEIT sie reicht -- vorher hiess blassErlaubt
schlicht „hier wird weggesehen".
DIE NEUE FARBE IST GRUEN UND WOLLTE BERNSTEIN SEIN. Gesucht war die
Farbe von Papier. Gemessen gibt es sie nicht mehr: Der beste Bernstein
im ganzen Farbraum haelt 0,0799 Abstand -- unter der Hausgrenze. Und
Platz schaffen hilft nicht: Setzt man ihn fest, muss ein warmer Ton
das Band verlassen (Ton 45 waere 0,212 weit ins Magenta gewandert).
Die groesste wirklich freie Luecke liegt im Gruen, bei 0,0917. Ein
linierter Block in Gruen ist Papier, seit es Papier gibt.
=================================================================
TEIL 2: DER BLOCK
=================================================================
DREI ENTSCHEIDUNGEN, AUS DENEN DER REST FOLGT:
1. ES GIBT KEINEN SPEICHERN-KNOPF. Nirgends. Wer einen Block
aufschlaegt und lostippt, drueckt hinterher nicht auf „sichern";
er klappt ihn zu. Gesichert wird 800 ms nach dem letzten
Tastendruck. Geht das nicht, bleibt der Text im Browser liegen und
wird nachgereicht -- und der Fuss sagt ehrlich „noch nicht
gesichert", statt einen Verlust zu melden, den man gerade nicht
verhindern kann.
2. DAS BLATT IST EIN SCHREIBFELD MIT EINER DECKSCHICHT. Das Feld
traegt Text und Cursor und ist unsichtbar; darueber zeichnet eine
zweite Schicht denselben Text noch einmal -- mit Kaestchen,
Ueberschriften, Strichen und Links.
DARAUS FOLGT EINE EISERNE REGEL, und sie steht dreimal im Code:
KEINE Auszeichnung darf die BREITE eines Zeichens aendern. Kein
Fettdruck, keine andere Groesse, keine Sperrung. Erlaubt sind nur
Farbe, Flaeche, Rahmen und Durchstreichen. Ein einziges
font-weight: 700 verschoebe den Umbruch, und ab der zweiten Zeile
stuende der sichtbare Text neben dem Cursor.
pruef-notizen misst das am echten Umbruch: eine Probe mit allen
Auszeichnungen und einer Zeile, die umbrechen MUSS. Sind beide
Schichten verschieden hoch, sitzt der Umbruch woanders. Mit
Gegenprobe -- Fettdruck auf der Deckschicht bricht die Deckung um
genau eine Zeile (30 px), und die Zeile wird rot.
3. DIE TASTATUR FUEHRT DIE LISTE FORT, NICHT EINE LEISTE. Ein
Spiegelstrich und Enter macht den naechsten Punkt, ein leeres
Kaestchen und Enter das naechste Kaestchen -- und ein GESETZTER
Haken wird dabei nicht mitgenommen, die naechste Aufgabe ist ja
noch nicht erledigt. Zweimal Enter beendet die Liste. Ein Klick
aufs Kaestchen hakt ab. Es gibt keine Werkzeugleiste, weil man beim
Schreiben nie den Stift wechselt.
UND: NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
Es gibt in workspace-notizen.js keinen siehtAlles()-Zweig, keinen
Umschalter, keine fremde Liste -- jede Abfrage hat person_id = ? fest
eingebaut. Die Kachel steht deshalb in „Fuer dich" und nicht in
„Taeglich": Die Gruppe sagt die wichtigste Eigenschaft, bevor man sie
anfasst.
NUR AUF DER TEAM-ADRESSE. Auf der Agenturadresse ist die Seite ein
404 -- auch fuer DogFather. Das ist dieselbe Trennung wie bei Chat,
Kalender und Aufgaben, und sie steht an zwei Stellen: in
GEHOERT_ZU_ADRESSE (die Datei) und in der Schnittstelle (die Daten).
Die Seite ist nur HTML; die Daten sind die Sache.
Dazu: sechs Papierfarben, Anheften, Suche mit Hervorhebung im Text,
Papierkorb mit dreissig Tagen und einem Eintrag in der
Aufbewahrungsliste (ohne den waere die Frist ein Satz in einem
Kommentar -- genau das ist dem Support am 24.09. passiert).
=================================================================
EIN FUND BEIM BAUEN, DER ALLEN GEHOERT
=================================================================
DELETE /workspace/api/notizen/:id gab es schon -- in
workspace-aufgaben.js, fuer die Notizen AN einer Aufgabe. Weil jener
Router frueher eingehaengt ist, hat er jeden Loeschversuch des Blocks
abgefangen und mit 404 beantwortet.
Von aussen sah das aus wie ein Fehler im neuen Modul: Anlegen ging,
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und dort
ist nichts. Express meldet so etwas nicht -- es nimmt die erste Route,
die passt, und schweigt ueber die zweite.
Der Block liegt jetzt unter /workspace/api/notizblock. Und
pruef-notizen geht seitdem den Routenbaum von express durch und meldet
jedes Paar aus Methode und Pfad, das zweimal vergeben ist. Gemessen:
307 Wege, keine Dublette. Die Pruefung gilt fuers ganze Haus, auch
wenn sie in dieser Datei steht -- hier ist sie gefunden worden.
Nebenbei berichtigt: pruef-crew-adresse verlangte von jedem Eintrag
der Haustafel HTTP 200 mit Inhalt. Das stimmte, solange dort nur
oeffentliche Dateien standen. notizen.html ist die erste Seite hinter
der Anmeldung -- sie antwortet mit 302, und das ist richtig. Gefragt
wird jetzt, was die Tafel wirklich meint: GIBT es das hier.
GEMESSEN, alles nach den Aenderungen:
pruef-notizen 76 Punkte, 0 Fehler (neu)
pruef-kachelfarben 31 Punkte, 0 Fehler (vorher 26)
pruef-kachel-universum 13 Punkte, 0 Fehler
pruef-crew-adresse 157 Punkte, 0 Fehler
pruef-buehne 230 Punkte, 0 Fehler -- notizen.html neu in
der Liste, schlechtester Kontrast 6,73:1,
also 50 % ueber der Grenze
pruef-rechtetafel, -haus-seiten, -struktur, -css-klassen,
-jeder-hat-eine-seite, -workspace-seiten, -kachelraster,
-rueckmeldung alle 0 Fehler
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -376,6 +376,16 @@ export function istWorkspacePfad(pfad) {
|
||||
export const GEHOERT_ZU_ADRESSE = [
|
||||
{ datei: "/workspace/crew-index.html", zuhause: istCrewAdresse },
|
||||
{ datei: "/workspace/assets/css/crew-haus.css", zuhause: istCrewAdresse },
|
||||
/* DER NOTIZBLOCK (26.09.2026). Er gehoert zum Haus von Team Dogi,
|
||||
und zwar vollstaendig: Auf der Agenturadresse gibt es die Seite
|
||||
nicht -- auch nicht fuer DogFather, der in beiden Haeusern
|
||||
arbeitet. Das ist dieselbe Trennung wie bei Chat, Kalender und
|
||||
Aufgaben; ohne diesen Eintrag waere die Seite dort einfach
|
||||
abrufbar, weil express.static jede Datei ausliefert, die man
|
||||
beim Namen nennt. Die Schnittstelle prueft es ein zweites Mal
|
||||
(nurTeamDogi in workspace-notizen.js) -- die Seite ist nur HTML,
|
||||
die Daten sind die Sache. */
|
||||
{ datei: "/workspace/notizen.html", zuhause: istCrewAdresse },
|
||||
];
|
||||
|
||||
/** Wird hier eine Wand verlangt, die zu einer ANDEREN Tuer gehoert? */
|
||||
|
||||
@@ -38,6 +38,7 @@ import { uebernahmeRouter } from "./workspace-uebernahme.js";
|
||||
import { befindenRouter } from "./workspace-befinden.js";
|
||||
import { hilfeRouter } from "./workspace-hilfe.js";
|
||||
import { supportRouter } from "./workspace-support.js";
|
||||
import { notizenRouter } from "./workspace-notizen.js";
|
||||
import { unterstuetzungRouter } from "./workspace-unterstuetzung.js";
|
||||
import { reportRouter } from "./workspace-reports.js";
|
||||
import { scoutRouter } from "./workspace-scouts.js";
|
||||
@@ -312,6 +313,10 @@ app.use(uebernahmeRouter);
|
||||
app.use(befindenRouter);
|
||||
app.use(hilfeRouter);
|
||||
app.use(supportRouter);
|
||||
/* Der Notizblock (26.09.2026): vier Rollen des Teamhauses, und jeder
|
||||
sieht nur seinen eigenen -- auch DogFather. Begruendung im Kopf von
|
||||
notiz-tabellen.js. */
|
||||
app.use(notizenRouter);
|
||||
/* Unterstützen (24.09.2026): die Wege, auf denen die Community etwas
|
||||
beitragen kann – lesen alle, ändern nur DogFather. */
|
||||
app.use(unterstuetzungRouter);
|
||||
@@ -569,3 +574,17 @@ process.on("uncaughtException", (fehler) => console.error("[uncaughtException]",
|
||||
app.listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`DogFather Universe – Server läuft auf http://127.0.0.1:${PORT}`);
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
DIE ANWENDUNG NACH AUSSEN (26.09.2026)
|
||||
|
||||
Nur fuer Pruefungen. `pruef-notizen` geht damit den Routenbaum von
|
||||
express durch und sucht Wege, die zweimal vergeben sind -- genau so
|
||||
ein Fall hat beim Bau des Notizblocks eine Stunde gekostet
|
||||
(DELETE /workspace/api/notizen/:id gab es schon in
|
||||
workspace-aufgaben.js, und express nimmt schweigend die erste).
|
||||
|
||||
Der Export aendert am Betrieb nichts: Wer diese Datei importiert,
|
||||
startet den Server ohnehin -- das tut sie beim Laden. Er macht nur
|
||||
sichtbar, was vorher nur im Speicher stand. */
|
||||
export default app;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/* =====================================================================
|
||||
WIE SIEHT DER NOTIZBLOCK AUS? (26.09.2026)
|
||||
|
||||
Ein Messwerkzeug, keine Pruefung: Es meldet nichts, es macht
|
||||
Bilder. Rechner und Handy, leeres Blatt und beschriebenes.
|
||||
|
||||
node server/mess-notizblock.mjs
|
||||
|
||||
Filipe: „für die modis, rechte und linke hand und dogfather eine
|
||||
kachel hinzufügst, sie soll: Notizen, heißen. ich will dass du das
|
||||
auch wie ein notizblock erstellst."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DREI DINGE WERDEN GEPRÜFT, UND ZWEI DAVON HAT NIEMAND BESTELLT
|
||||
|
||||
1. WER DARF ÜBERHAUPT. Genau die vier Rollen, und niemand sonst —
|
||||
auch nicht über die Schnittstelle, auch nicht von der anderen
|
||||
Adresse aus. Die Rollenmenge wird dabei nicht abgeschrieben,
|
||||
sondern aus dem Programm gelesen und gegen die Rechtetafel
|
||||
gehalten: Zwei Listen für dieselbe Frage laufen auseinander.
|
||||
|
||||
2. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
|
||||
Das ist die Zusage, die auf der Kachel steht („nur du siehst
|
||||
ihn"), und die einzige, deren Bruch man nicht bemerken würde:
|
||||
Eine Notiz, die jemand Fremdes liest, sieht für den Schreiber
|
||||
genauso aus wie eine, die niemand liest.
|
||||
|
||||
3. DIE DECKSCHICHT LIEGT AUF DEN PIXEL GENAU. Das Blatt besteht
|
||||
aus einem unsichtbaren Schreibfeld und einer sichtbaren
|
||||
Deckschicht darüber. Ändert eine Auszeichnung die BREITE eines
|
||||
Zeichens (Fettdruck, andere Schriftgröße, Sperrung), brechen
|
||||
beide Schichten an verschiedenen Stellen um — und der Cursor
|
||||
steht neben dem Text. Das sieht aus wie ein Browserfehler und
|
||||
wäre einer in notizen.css. Gemessen wird deshalb am echten
|
||||
Umbruch, nicht an der Absicht.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DIESE PRÜFUNG ÜBER https SPRICHT
|
||||
|
||||
Derselbe Grund wie bei pruef-handy-teamdogi: `dogfather-universe.com`
|
||||
steht in der fest eingebauten HSTS-Liste von Chromium, und ohne die
|
||||
Team-Adresse gibt es diese Seite gar nicht — auf einer Prüfadresse
|
||||
(127.0.0.1) ist `haus` weder „crew" noch „agentur", und der Block
|
||||
antwortet zu Recht mit 404.
|
||||
|
||||
Aufrufen mit: node server/pruef-notizen.mjs
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request } from "node:http";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-notiz-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
|
||||
/* Eine feste Nummer -- `eigenerPort` leitet seine aus der Stelle im
|
||||
Alphabet der PRUEFdateien ab, und dieses Werkzeug ist keine.
|
||||
Geprueft wird trotzdem, ob sie frei ist: Sonst misst man an einem
|
||||
fremden Server und bekommt ein Bild von einem anderen Stand. */
|
||||
const PORT = 5461;
|
||||
{
|
||||
const netz = await import("node:net");
|
||||
const frei = await new Promise((fertig) => {
|
||||
const s = netz.createServer();
|
||||
s.once("error", () => fertig(false));
|
||||
s.once("listening", () => s.close(() => fertig(true)));
|
||||
s.listen(PORT, "127.0.0.1");
|
||||
});
|
||||
if (!frei) { console.error(`Port ${PORT} ist belegt. Abgebrochen.`); process.exit(3); }
|
||||
}
|
||||
|
||||
process.env.PORT = `${PORT}`;
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
notbremse(200_000, "mess-notizblock");
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
|
||||
let fehler = 0, geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
const melde = (t) => console.log(t);
|
||||
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
|
||||
|
||||
const CREW = "crew.dogfather-universe.com";
|
||||
const AGENTUR = "workspace.dogfather-universe.com";
|
||||
|
||||
/* ---------- Zugänge anlegen -------------------------------------------- */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const nun = new Date().toISOString();
|
||||
const kennSchluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", kennSchluessel, nun);
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
|
||||
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768,
|
||||
createHmac("sha256", kennSchluessel).update(code).digest("hex"), nun);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
||||
["modi", "Miss"], ["gast", "Zuschauerin"], ["creator", "Luna"]];
|
||||
const CODES = {};
|
||||
const IDS = {};
|
||||
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
||||
CODES[rolle] = `CODE-N${String(i).padStart(2, "0")}-000001`;
|
||||
IDS[rolle] = anlegen(name, rolle, CODES[rolle]);
|
||||
}
|
||||
d.close();
|
||||
|
||||
/* ---------- Sprechen mit Hausadresse ------------------------------------
|
||||
`fetch` kann die Host-Kopfzeile nicht setzen (verbotene Kopfzeile),
|
||||
und an ihr entscheidet das Haus alles. Deshalb node:http. */
|
||||
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
|
||||
return new Promise((fertig, schief) => {
|
||||
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
|
||||
const a = request({
|
||||
host: "127.0.0.1", port: PORT, path: pfad, method,
|
||||
headers: {
|
||||
Host: host,
|
||||
Origin: origin === undefined ? `https://${host}` : origin,
|
||||
...(keks ? { Cookie: keks } : {}),
|
||||
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
||||
},
|
||||
}, (antwort) => {
|
||||
const teile = [];
|
||||
antwort.on("data", (s) => teile.push(s));
|
||||
antwort.on("end", () => {
|
||||
const text = Buffer.concat(teile).toString("utf8");
|
||||
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
|
||||
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
|
||||
});
|
||||
});
|
||||
a.on("error", schief);
|
||||
if (daten) a.write(daten);
|
||||
a.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function anmelden(rolle, host) {
|
||||
const a = await roh(host, "/workspace/api/anmelden", {
|
||||
method: "POST", rumpf: { rolle, code: CODES[rolle] },
|
||||
});
|
||||
const keks = []
|
||||
.concat(a.kopf["set-cookie"] || [])
|
||||
.map((z) => z.split(";")[0]).join("; ");
|
||||
return a.code === 200 && keks ? keks : null;
|
||||
}
|
||||
|
||||
const kekse = { modi: await anmelden("modi", CREW) };
|
||||
if (!kekse.modi) { console.error("Anmeldung ging nicht"); process.exit(1); }
|
||||
|
||||
const PROBE = [
|
||||
"# Sendung am Freitag",
|
||||
"",
|
||||
"[x] Gaesteliste an VanVan",
|
||||
"[ ] Technik durchgehen -- Ton am Mischpult",
|
||||
"[ ] Ansage schreiben",
|
||||
"",
|
||||
"- Idee: Zuschauerfragen in der letzten halben Stunde",
|
||||
"- Idee: Gewinnspiel, aber ohne Punkte",
|
||||
"",
|
||||
"> Nicht vergessen: Casper braucht um 19 Uhr seine Runde.",
|
||||
"",
|
||||
"Unterlagen: https://crew.dogfather-universe.com/workspace/dateien.html",
|
||||
].join(String.fromCharCode(10));
|
||||
|
||||
const pw = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const { execFileSync } = await import("node:child_process");
|
||||
const { createServer: httpsServer } = await import("node:https");
|
||||
const { request: httpAnfrage } = await import("node:http");
|
||||
const schluesselDatei = join(ordner, "m.key");
|
||||
const zertDatei = join(ordner, "m.crt");
|
||||
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
|
||||
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
|
||||
const HTTPS_PORT = PORT + 1;
|
||||
const vorbau = httpsServer({ key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei) },
|
||||
(von, nach) => {
|
||||
const weiter = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url,
|
||||
method: von.method, headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` } },
|
||||
(antwort) => { nach.writeHead(antwort.statusCode || 502, antwort.headers); antwort.pipe(nach); });
|
||||
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
|
||||
von.pipe(weiter);
|
||||
});
|
||||
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
||||
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
|
||||
|
||||
/* Drei Blaetter, damit der Stapel etwas zeigt. */
|
||||
const BR = String.fromCharCode(10);
|
||||
await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
||||
rumpf: { text: ["Einkauf fuers Studio", "[ ] Kabel", "[ ] Kaffee"].join(BR) } });
|
||||
await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
||||
rumpf: { text: ["Woerter, die ich nicht mehr sagen will", "- eigentlich", "- quasi"].join(BR),
|
||||
farbe: "rosa" } });
|
||||
const haupt = await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
||||
rumpf: { text: PROBE, farbe: "sonne" } });
|
||||
await roh(CREW, `/workspace/api/notizblock/${haupt.j.notiz.id}`, { method: "PATCH",
|
||||
keks: kekse.modi, rumpf: { angeheftet: true } });
|
||||
|
||||
const browser = await pw.chromium.launch({
|
||||
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"] });
|
||||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 980 },
|
||||
ignoreHTTPSErrors: true, deviceScaleFactor: 2 });
|
||||
await kontext.addCookies(kekse.modi.split("; ").filter(Boolean).map((k) => {
|
||||
const [n, ...r] = k.split("=");
|
||||
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
||||
}));
|
||||
|
||||
const seite = await kontext.newPage();
|
||||
await seite.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(1200);
|
||||
await seite.screenshot({ path: "server/mess-notizblock-rechner.png" });
|
||||
console.log("server/mess-notizblock-rechner.png");
|
||||
|
||||
const handy = await kontext.newPage();
|
||||
await handy.setViewportSize({ width: 390, height: 844 });
|
||||
await handy.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
||||
await handy.waitForTimeout(900);
|
||||
await handy.screenshot({ path: "server/mess-notizblock-handy-liste.png" });
|
||||
await handy.locator(".n-zettel").first().click();
|
||||
await handy.waitForTimeout(600);
|
||||
await handy.screenshot({ path: "server/mess-notizblock-handy-blatt.png" });
|
||||
console.log("server/mess-notizblock-handy-liste.png");
|
||||
console.log("server/mess-notizblock-handy-blatt.png");
|
||||
|
||||
await browser.close();
|
||||
await new Promise((r) => vorbau.close(r));
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,157 @@
|
||||
/* =====================================================================
|
||||
DER NOTIZBLOCK — die Tabelle und die Regeln dahinter (26.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Filipe: „für die modis, rechte und linke hand und dogfather eine
|
||||
kachel hinzufügst, sie soll: Notizen, heißen. ich will dass du das
|
||||
auch wie ein notizblock erstellst."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIE EINE ENTSCHEIDUNG, DIE ALLES ANDERE NACH SICH ZIEHT
|
||||
|
||||
EIN NOTIZBLOCK GEHÖRT EINEM MENSCHEN. Nicht dem Team, nicht dem
|
||||
Haus, nicht der Leitung. Wer seine Gedanken aufschreibt, schreibt
|
||||
anders, wenn jemand mitlesen könnte — und dann ist es kein Block
|
||||
mehr, sondern ein Bericht.
|
||||
|
||||
Deshalb gibt es hier KEINE Rolle, die mehr sieht. `siehtAlles()`
|
||||
wird in diesem Modul nicht gefragt, und das ist Absicht: DogFather
|
||||
sieht die Notizen eines Modis so wenig wie umgekehrt. Jede Abfrage
|
||||
hat `person_id = ?` fest eingebaut, nicht als Bedingung, die man
|
||||
weglassen könnte.
|
||||
|
||||
Das ist dieselbe Linie wie bei `befinden.html` („Ein paar Fragen
|
||||
über dich, nur für dich. Niemand aus dem Team sieht deine
|
||||
Antworten") — dort steht sie schon im Haus, hier wird sie
|
||||
fortgesetzt.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM ON DELETE CASCADE UND NICHT SET NULL
|
||||
|
||||
Bei Aufgaben ist es richtig, den Namen zu entfernen und die Sache
|
||||
stehen zu lassen: Sie gehört dem Projekt. Eine Notiz gehört
|
||||
niemandem sonst. Wird ein Zugang gelöscht, hat die Zeile keinen
|
||||
Leser mehr — sie wäre ein Text ohne Menschen, den irgendwann jemand
|
||||
in der Datenbank findet. Also geht sie mit.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DER PAPIERKORB IST EIN DATUM, KEINE ZWEITE TABELLE
|
||||
|
||||
`weg_am` gesetzt heißt „liegt im Papierkorb, seit …". Eine zweite
|
||||
Tabelle wäre dieselbe Struktur noch einmal, und beim nächsten neuen
|
||||
Feld hätte eine davon es nicht. Geräumt wird über die
|
||||
Aufbewahrungsliste (workspace-aufbewahrung.js) — dort steht die
|
||||
Frist, dort wird sie eingelöst. Ein Versprechen ohne Eintrag dort
|
||||
ist kein Versprechen; genau das ist dem Support am 24.09. passiert.
|
||||
===================================================================== */
|
||||
|
||||
/** Wie lange etwas im Papierkorb liegt, bevor es wirklich weg ist.
|
||||
*
|
||||
* Dreißig Tage. Kürzer wäre eine Falle für den, der drei Wochen im
|
||||
* Urlaub war; länger würde aus dem Papierkorb ein zweites Archiv,
|
||||
* und dann räumt ihn niemand mehr aus. */
|
||||
export const PAPIERKORB_TAGE = 30;
|
||||
|
||||
/** Wie lang eine einzelne Notiz werden darf.
|
||||
*
|
||||
* Zwanzigtausend Zeichen sind rund acht eng beschriebene Seiten.
|
||||
* Die Zahl steht nicht gegen den Benutzer, sondern gegen ein
|
||||
* hängendes Skript: Ohne Grenze schreibt ein Fehler in einer Schleife
|
||||
* die Platte voll, und das merkt man erst, wenn nichts mehr geht. */
|
||||
export const TEXT_MAX = 20000;
|
||||
|
||||
/** Wie viele Blätter ein Block hat.
|
||||
*
|
||||
* Dreihundert. Wer so viele hat, sucht ohnehin über die Suche und
|
||||
* nicht über die Liste — und auch diese Zahl steht gegen ein Skript,
|
||||
* nicht gegen einen Menschen. */
|
||||
export const BLAETTER_MAX = 300;
|
||||
|
||||
/* =====================================================================
|
||||
DIE PAPIERFARBEN
|
||||
|
||||
Sechs, und keine davon ist ein Papierweiß. Auf einer dunklen Wand
|
||||
wäre ein cremefarbenes Blatt eine Lampe — die Hausregel heißt
|
||||
augenschonend, und sie gilt auch für ein Blatt Papier.
|
||||
|
||||
Die Farbe ist deshalb die TINTE: Rand, Linien und Kante des Blattes
|
||||
tragen sie, die Fläche bleibt dunkel. Das ist näher an einer
|
||||
schwarzen Kladde mit farbigem Lesebändchen als an einem Klebezettel
|
||||
— und man kann sechs Blätter nebeneinander auseinanderhalten, ohne
|
||||
dass eines blendet.
|
||||
|
||||
ALLE SECHS HALTEN 4,5:1 GEGEN DEN DUNKLEN GRUND (#0a121e) —
|
||||
gemessen, nicht geschätzt, und nachgeprüft in pruef-notizen. Sie
|
||||
tragen ja auch Text.
|
||||
|
||||
WARUM NICHT DIE 46 KACHELFARBEN: Die sind gegeneinander
|
||||
auseinandergerechnet, damit man 46 Kacheln unterscheidet. Hier
|
||||
müssen sechs auseinandergehen, und dafür sind größere, ruhigere
|
||||
Abstände besser als sechs Nachbarn aus einer vollen Palette. */
|
||||
export const FARBEN = [
|
||||
{ name: "block", wort: "Block", hex: "#9cde90" },
|
||||
{ name: "sonne", wort: "Sonne", hex: "#e8bd63" },
|
||||
{ name: "rosa", wort: "Rosa", hex: "#f2a0c0" },
|
||||
{ name: "himmel", wort: "Himmel", hex: "#86c5f5" },
|
||||
{ name: "flieder", wort: "Flieder", hex: "#c3a6f2" },
|
||||
{ name: "minze", wort: "Minze", hex: "#7ee0c4" },
|
||||
];
|
||||
export const FARB_NAMEN = FARBEN.map((f) => f.name);
|
||||
|
||||
/**
|
||||
* Legt die Tabelle an. Wird aus workspace.js gerufen, nachdem
|
||||
* `personen` steht — die Notiz hängt daran.
|
||||
*
|
||||
* ACHTUNG, EIN GEGEN-APOSTROPH IN DIESEM TEXT BEENDET DAS
|
||||
* TEMPLATE-LITERAL. Das hat in diesem Haus schon zweimal den Start
|
||||
* gekostet (support-tabellen.js, workspace.js). Deshalb steht in den
|
||||
* Kommentaren hier drin kein einziger.
|
||||
*/
|
||||
export function notizTabellen(d) {
|
||||
d.exec(`
|
||||
CREATE TABLE IF NOT EXISTS notizen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* CASCADE: Eine Notiz ohne ihren Menschen ist ein Text, den
|
||||
niemand lesen darf und niemand vermisst. */
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
/* DER GANZE TEXT IN EINEM FELD -- Titel, Haken, Absaetze.
|
||||
|
||||
Eine eigene Titelspalte waere die naheliegende Loesung und
|
||||
die falsche: Dann muesste die Oberflaeche zwei Eingabefelder
|
||||
haben, und ein Notizblock mit Formular ist kein Notizblock
|
||||
mehr. Die erste Zeile IST der Titel, und zwar dort, wo sie
|
||||
hingehoert -- im Text. */
|
||||
text TEXT NOT NULL DEFAULT '',
|
||||
farbe TEXT NOT NULL DEFAULT 'block',
|
||||
/* Angeheftet: bleibt oben. Als Zahl und nicht als Datum -- hier
|
||||
geht es um ein Ja oder Nein, und ein Datum waere ein Feld,
|
||||
das etwas anderes verspricht, als es haelt. */
|
||||
angeheftet INTEGER NOT NULL DEFAULT 0,
|
||||
erstellt TEXT NOT NULL,
|
||||
geaendert TEXT NOT NULL,
|
||||
/* Im Papierkorb seit. NULL heisst: auf dem Block. */
|
||||
weg_am TEXT
|
||||
);
|
||||
|
||||
/* Die eine Frage, die dieses Modul stellt: „meine Blaetter, die
|
||||
angehefteten zuerst, danach das zuletzt Angefasste". Der Index
|
||||
beantwortet sie in genau dieser Reihenfolge. */
|
||||
CREATE INDEX IF NOT EXISTS idx_notizen_person
|
||||
ON notizen (person_id, weg_am, angeheftet DESC, geaendert DESC);
|
||||
/* Und die Frage des Aufraeumers: „was liegt lange genug im
|
||||
Papierkorb?" */
|
||||
CREATE INDEX IF NOT EXISTS idx_notizen_weg
|
||||
ON notizen (weg_am);
|
||||
`);
|
||||
|
||||
/* DIE FARBPRUEFUNG STEHT NICHT IN DER TABELLE.
|
||||
|
||||
Ein CHECK (farbe IN (...)) waere die naheliegende Stelle -- und
|
||||
die, an der die Liste ein zweites Mal steht. Waechst FARBEN oben
|
||||
um eine Farbe, muesste die Tabelle umgebaut werden, damit ein
|
||||
Wert erlaubt wird, der im Programm laengst gilt. Genau diese
|
||||
Sorte Abschrift hat am 11.09. drei Spalten gekostet.
|
||||
|
||||
Geprueft wird deshalb an der Route, gegen FARB_NAMEN -- eine
|
||||
Liste, die niemand pflegen muss, weil sie abgeleitet ist. */
|
||||
}
|
||||
@@ -73,6 +73,10 @@ const ALLE_SEITEN = [
|
||||
/* Neu am 21.09.2026 -- sie traegt eine Buehne und viel Text. */
|
||||
"willkommen.html",
|
||||
"unsere-seiten.html",
|
||||
/* Neu am 26.09.2026: der Notizblock. Er traegt eine Buehne, und
|
||||
auf ihm steht die leiseste Schrift des Hauses direkt ueber
|
||||
einem Foto -- genau der Fall, fuer den diese Datei gebaut ist. */
|
||||
"notizen.html",
|
||||
];
|
||||
const SEITEN = process.env.BUEHNE_SEITEN
|
||||
? process.env.BUEHNE_SEITEN.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
|
||||
@@ -645,11 +645,36 @@ for (const eintrag of GEHOERT_ZU_ADRESSE) {
|
||||
ok((draussen.text || "").length < 200,
|
||||
`workspace.: und es kommt auch kein Inhalt mit (${(draussen.text || "").length} Zeichen)`);
|
||||
|
||||
/* ==== DRINNEN HEISST NICHT IMMER 200 (26.09.2026) ==============
|
||||
|
||||
Hier stand „muss 200 sein, und mehr als 200 Zeichen lang".
|
||||
Das stimmte, solange in der Tafel nur OEFFENTLICHE Dateien
|
||||
standen -- die Eingangswand und eine Stilvorlage.
|
||||
|
||||
Mit notizen.html steht dort zum ersten Mal eine Seite HINTER
|
||||
der Anmeldung. Ohne Sitzung antwortet sie mit 302 auf die
|
||||
Anmeldung, und das ist richtig so. Die Pruefung meldete
|
||||
trotzdem einen Fehler -- nicht am Haus, sondern an ihrer
|
||||
eigenen, zu engen Erwartung.
|
||||
|
||||
Die Frage dieser Tafel ist nicht „liefert es Inhalt aus?",
|
||||
sondern „GIBT es das hier?". Genau das wird jetzt gemessen:
|
||||
drinnen alles ausser 404/410, draussen nur 404/410. Beide
|
||||
Richtungen bleiben, nur die Innenseite ist nicht mehr auf eine
|
||||
einzige Zahl festgelegt. */
|
||||
const drinnen = await hole(weg, { host: CREW });
|
||||
ok(drinnen.status === 200,
|
||||
`crew.: ${weg} kommt dort an (${drinnen.status})`);
|
||||
ok((drinnen.text || "").length > 200,
|
||||
`crew.: und zwar mit Inhalt (${(drinnen.text || "").length} Zeichen)`);
|
||||
ok(drinnen.status !== 404 && drinnen.status !== 410,
|
||||
`crew.: ${weg} gibt es dort (${drinnen.status})`);
|
||||
/* Und eine offene Datei kommt dort auch wirklich an. Bei einer
|
||||
Seite hinter der Anmeldung waere „mehr als 200 Zeichen" eine
|
||||
Aussage ueber die Weiterleitung, nicht ueber die Seite. */
|
||||
if (drinnen.status === 200) {
|
||||
ok((drinnen.text || "").length > 200,
|
||||
`crew.: und zwar mit Inhalt (${(drinnen.text || "").length} Zeichen)`);
|
||||
} else {
|
||||
ok(drinnen.status === 302 || drinnen.status === 401 || drinnen.status === 403,
|
||||
`crew.: und schickt ohne Anmeldung zur Anmeldung (${drinnen.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
/* DIE UMBIEGUNG DARF DIE SPERRE NICHT MITNEHMEN.
|
||||
|
||||
@@ -133,6 +133,63 @@ for (const r of rollen) {
|
||||
|
||||
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG. Fände diese Prüfung null benutzte
|
||||
Töne, wäre alles Folgende trivial grün — und bliebe es für immer. */
|
||||
/* =====================================================================
|
||||
DIE ZWEITE QUELLE -- UND DER BLINDE FLECK, DEN SIE SCHLIESST
|
||||
===================================================================== (26.09.2026)
|
||||
|
||||
`benutzt` oben entsteht aus bereicheFuer() je Rolle. Das ist die
|
||||
richtige Quelle fuer die Frage „welche Kachel traegt welchen Ton
|
||||
in welcher Ansicht" -- und es ist die falsche fuer die Frage
|
||||
„welche Toene sind ueberhaupt in Gebrauch".
|
||||
|
||||
Denn fuer die fuenf Rollen des AGENTURHAUSES gibt bereicheFuer()
|
||||
`null` zurueck. Das heisst „nimm die Liste aus dem Browser", und
|
||||
die steht in workspace/assets/js/bereiche.js. Dort stehen die
|
||||
Toene 1 bis 22 und 44 -- zweiundzwanzig Stueck, und keiner davon
|
||||
ist hier je angekommen.
|
||||
|
||||
WAS DAS GEKOSTET HAT: Diese Pruefung meldete „33 benutzte Toene"
|
||||
und war gruen. Am 25.09. hat ein Farblauf daraufhin zwoelf
|
||||
vermeintlich freie Toene verschoben und drei davon unter die
|
||||
Buntheitsgrenze gedrueckt -- darunter „Dashboard", „Zahlen" und
|
||||
„Scout-Pipeline", Kacheln, die jeden Tag jemand ansieht. Die
|
||||
Pruefung blieb gruen. Ausserdem lagen Ton 7, 16 und 22 SEIT JEHER
|
||||
unter der Grenze, ohne dass es je gemeldet wurde.
|
||||
|
||||
Das ist die Sorte gruener Haken, vor der die Hausregel warnt: Er
|
||||
sagt nur, dass die Bedingung erfuellt war -- nicht, dass sie das
|
||||
Richtige angesehen hat.
|
||||
|
||||
Ab hier gelten die REGELN (Kontrast, Buntheit) fuer jeden Ton, der
|
||||
irgendwo an einer Kachel steht. Gelesen werden dieselben fuenf
|
||||
Quellen wie in pruef-kachel-universum. Die Fragen nach ANSICHTEN
|
||||
(zwei gleiche nebeneinander, die Willkommenskachel) bleiben bei
|
||||
`benutzt` -- dort geht es wirklich um das, was der Server
|
||||
ausgibt. */
|
||||
const QUELLEN = ["workspace.js", "../workspace/assets/js/bereiche.js",
|
||||
"../workspace/assets/js/start.js", "../workspace/assets/js/kopf.js",
|
||||
"../workspace/assets/js/befinden.js"];
|
||||
const IN_GEBRAUCH = new Set();
|
||||
/* Und WELCHE Kachel einen Ton traegt -- auch das aus den Quellen,
|
||||
nicht nur aus dem, was der Server ausgibt. Gesucht wird der Name
|
||||
VOR der Tonnummer im selben Objekt; `[^{}]*?` sorgt dafuer, dass
|
||||
die Suche an der naechsten geschweiften Klammer endet und nicht
|
||||
den Namen der vorigen Kachel einsammelt. */
|
||||
const TRAEGT = new Map();
|
||||
for (const q of QUELLEN) {
|
||||
const text = readFileSync(join(HIER, q), "utf8");
|
||||
for (const m of text.matchAll(/ton:\s*(\d+)/g)) IN_GEBRAUCH.add(Number(m[1]));
|
||||
for (const m of text.matchAll(/name:\s*["']([^"']+)["'][^{}]*?ton:\s*(\d+)/g)) {
|
||||
const n = Number(m[2]);
|
||||
if (!TRAEGT.has(n)) TRAEGT.set(n, new Set());
|
||||
TRAEGT.get(n).add(m[1]);
|
||||
}
|
||||
}
|
||||
pruef(`${IN_GEBRAUCH.size} Toene stehen an einer Kachel -- gelesen aus `
|
||||
+ `${QUELLEN.length} Dateien, nicht nur aus dem, was der Server ausgibt`,
|
||||
IN_GEBRAUCH.size >= benutzt.size,
|
||||
`nur ${IN_GEBRAUCH.size} gefunden, der Server gibt schon ${benutzt.size} aus`);
|
||||
|
||||
pruef(`es gibt benutzte Kachelfarben (${benutzt.size} Toene, ${ansichten.length} Ansichten)`,
|
||||
benutzt.size >= 20 && ansichten.length >= 5,
|
||||
`nur ${benutzt.size} Toene in ${ansichten.length} Ansichten gefunden`);
|
||||
@@ -243,7 +300,13 @@ console.log("\n=== Keine Ansicht zeigt zwei gleiche ===");
|
||||
/* ---- 3. Festgelegt -------------------------------------------------- */
|
||||
console.log("\n=== Was festgelegt ist, bleibt ===");
|
||||
for (const f of FESTGELEGT) {
|
||||
const traeger = [...(benutzt.get(f.ton) || [])];
|
||||
/* AUS BEIDEN QUELLEN. Bis zum 26.09. stand hier nur `benutzt` --
|
||||
die Kacheln, die der Server ausgibt. Ton 1 traegt „Dashboard"
|
||||
im Agenturhaus, und das steht im Browser; die Zeile meldete
|
||||
deshalb „traegt stattdessen: nichts" fuer eine Kachel, die es
|
||||
gibt. */
|
||||
const traeger = [...new Set([...(benutzt.get(f.ton) || []),
|
||||
...(TRAEGT.get(f.ton) || [])])];
|
||||
pruef(`Ton ${f.ton} traegt "${f.kachel}" (${f.warum})`,
|
||||
traeger.includes(f.kachel),
|
||||
`Ton ${f.ton} traegt stattdessen: ${traeger.join(", ") || "nichts"}`);
|
||||
@@ -263,15 +326,23 @@ console.log("\n=== Lesbar und kraeftig ===");
|
||||
beim naechsten Mal vergessen wird. */
|
||||
const darfBlassSein = new Map(
|
||||
FESTGELEGT.filter((f) => f.blassErlaubt).map((f) => [f.ton, f.blassErlaubt]));
|
||||
for (const n of benutzt.keys()) {
|
||||
/* WIE BLASS GENAU -- eine Ausnahme mit Zahl statt eines Freibriefs
|
||||
(26.09.2026). Vorher hiess `blassErlaubt` schlicht „diese
|
||||
Pruefung sieht hier weg"; ein Ton haette danach beliebig
|
||||
verblassen koennen, ohne dass es auffaellt. Jetzt steht neben
|
||||
jeder Ausnahme, wie weit sie reicht. */
|
||||
const blassBis = new Map(
|
||||
FESTGELEGT.filter((f) => f.blassBis).map((f) => [f.ton, f.blassBis]));
|
||||
for (const n of IN_GEBRAUCH) {
|
||||
const hex = farben.get(n);
|
||||
if (!hex) continue;
|
||||
if (kontrast(hex) < 4.5) dunkel.push(`Ton ${n} ${hex} (${kontrast(hex).toFixed(2)}:1)`);
|
||||
if (buntheit(hex) < MINDEST_BUNTHEIT && !darfBlassSein.has(n)) {
|
||||
blass.push(`Ton ${n} ${hex} (${buntheit(hex).toFixed(3)})`);
|
||||
const grenze = blassBis.get(n) ?? MINDEST_BUNTHEIT;
|
||||
if (buntheit(hex) < grenze) {
|
||||
blass.push(`Ton ${n} ${hex} (${buntheit(hex).toFixed(3)}, erlaubt ab ${grenze})`);
|
||||
}
|
||||
}
|
||||
pruef(`alle ${benutzt.size} benutzten Toene erreichen 4,5:1 gegen den Grund`,
|
||||
pruef(`alle ${IN_GEBRAUCH.size} Toene an Kacheln erreichen 4,5:1 gegen den Grund`,
|
||||
dunkel.length === 0, dunkel.join(", "));
|
||||
pruef(`keiner ist blasser als ${MINDEST_BUNTHEIT}`
|
||||
+ (darfBlassSein.size ? ` (ausser ${darfBlassSein.size} benannten)` : ""),
|
||||
@@ -287,7 +358,7 @@ console.log("\n=== Lesbar und kraeftig ===");
|
||||
if (!hex) { pruef(`Ton ${ton} ist festgelegt, steht aber nicht in start.css`, false); continue; }
|
||||
let naechste = Infinity, wer = "-";
|
||||
for (const [m, h2] of farben) {
|
||||
if (m === ton || !benutzt.has(m)) continue;
|
||||
if (m === ton || !IN_GEBRAUCH.has(m)) continue;
|
||||
const d = abstand(oklab(hex), oklab(h2));
|
||||
if (d < naechste) { naechste = d; wer = `Ton ${m}`; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
/* =====================================================================
|
||||
DER NOTIZBLOCK (26.09.2026)
|
||||
|
||||
Filipe: „für die modis, rechte und linke hand und dogfather eine
|
||||
kachel hinzufügst, sie soll: Notizen, heißen. ich will dass du das
|
||||
auch wie ein notizblock erstellst."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DREI DINGE WERDEN GEPRÜFT, UND ZWEI DAVON HAT NIEMAND BESTELLT
|
||||
|
||||
1. WER DARF ÜBERHAUPT. Genau die vier Rollen, und niemand sonst —
|
||||
auch nicht über die Schnittstelle, auch nicht von der anderen
|
||||
Adresse aus. Die Rollenmenge wird dabei nicht abgeschrieben,
|
||||
sondern aus dem Programm gelesen und gegen die Rechtetafel
|
||||
gehalten: Zwei Listen für dieselbe Frage laufen auseinander.
|
||||
|
||||
2. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
|
||||
Das ist die Zusage, die auf der Kachel steht („nur du siehst
|
||||
ihn"), und die einzige, deren Bruch man nicht bemerken würde:
|
||||
Eine Notiz, die jemand Fremdes liest, sieht für den Schreiber
|
||||
genauso aus wie eine, die niemand liest.
|
||||
|
||||
3. DIE DECKSCHICHT LIEGT AUF DEN PIXEL GENAU. Das Blatt besteht
|
||||
aus einem unsichtbaren Schreibfeld und einer sichtbaren
|
||||
Deckschicht darüber. Ändert eine Auszeichnung die BREITE eines
|
||||
Zeichens (Fettdruck, andere Schriftgröße, Sperrung), brechen
|
||||
beide Schichten an verschiedenen Stellen um — und der Cursor
|
||||
steht neben dem Text. Das sieht aus wie ein Browserfehler und
|
||||
wäre einer in notizen.css. Gemessen wird deshalb am echten
|
||||
Umbruch, nicht an der Absicht.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DIESE PRÜFUNG ÜBER https SPRICHT
|
||||
|
||||
Derselbe Grund wie bei pruef-handy-teamdogi: `dogfather-universe.com`
|
||||
steht in der fest eingebauten HSTS-Liste von Chromium, und ohne die
|
||||
Team-Adresse gibt es diese Seite gar nicht — auf einer Prüfadresse
|
||||
(127.0.0.1) ist `haus` weder „crew" noch „agentur", und der Block
|
||||
antwortet zu Recht mit 404.
|
||||
|
||||
Aufrufen mit: node server/pruef-notizen.mjs
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request } from "node:http";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-notiz-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||||
const PORT = await eigenerPort(import.meta, "die Notizblockpruefung");
|
||||
|
||||
process.env.PORT = `${PORT}`;
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
notbremse(260_000, "pruef-notizen");
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
|
||||
let fehler = 0, geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
const melde = (t) => console.log(t);
|
||||
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
|
||||
|
||||
const CREW = "crew.dogfather-universe.com";
|
||||
const AGENTUR = "workspace.dogfather-universe.com";
|
||||
|
||||
/* ---------- Zugänge anlegen -------------------------------------------- */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const nun = new Date().toISOString();
|
||||
const kennSchluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", kennSchluessel, nun);
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
|
||||
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768,
|
||||
createHmac("sha256", kennSchluessel).update(code).digest("hex"), nun);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
||||
["modi", "Miss"], ["gast", "Zuschauerin"], ["creator", "Luna"]];
|
||||
const CODES = {};
|
||||
const IDS = {};
|
||||
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
||||
CODES[rolle] = `CODE-N${String(i).padStart(2, "0")}-000001`;
|
||||
IDS[rolle] = anlegen(name, rolle, CODES[rolle]);
|
||||
}
|
||||
d.close();
|
||||
|
||||
/* ---------- Sprechen mit Hausadresse ------------------------------------
|
||||
`fetch` kann die Host-Kopfzeile nicht setzen (verbotene Kopfzeile),
|
||||
und an ihr entscheidet das Haus alles. Deshalb node:http. */
|
||||
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
|
||||
return new Promise((fertig, schief) => {
|
||||
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
|
||||
const a = request({
|
||||
host: "127.0.0.1", port: PORT, path: pfad, method,
|
||||
headers: {
|
||||
Host: host,
|
||||
Origin: origin === undefined ? `https://${host}` : origin,
|
||||
...(keks ? { Cookie: keks } : {}),
|
||||
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
||||
},
|
||||
}, (antwort) => {
|
||||
const teile = [];
|
||||
antwort.on("data", (s) => teile.push(s));
|
||||
antwort.on("end", () => {
|
||||
const text = Buffer.concat(teile).toString("utf8");
|
||||
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
|
||||
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
|
||||
});
|
||||
});
|
||||
a.on("error", schief);
|
||||
if (daten) a.write(daten);
|
||||
a.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function anmelden(rolle, host) {
|
||||
const a = await roh(host, "/workspace/api/anmelden", {
|
||||
method: "POST", rumpf: { rolle, code: CODES[rolle] },
|
||||
});
|
||||
const keks = []
|
||||
.concat(a.kopf["set-cookie"] || [])
|
||||
.map((z) => z.split(";")[0]).join("; ");
|
||||
return a.code === 200 && keks ? keks : null;
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
1. WER DARF — und woher die Liste kommt
|
||||
======================================================================= */
|
||||
melde("=== 1. Wer einen Block hat ===");
|
||||
|
||||
const { ROLLEN_MIT_BLOCK } = await import("./workspace-notizen.js");
|
||||
const { SEITEN } = await import("./rechte.js");
|
||||
{
|
||||
const ausTafel = [...(SEITEN["/workspace/notizen.html"] || [])].sort();
|
||||
const ausProgramm = [...ROLLEN_MIT_BLOCK].sort();
|
||||
ok(ausTafel.length === 4,
|
||||
`die Rechtetafel nennt vier Rollen (${ausTafel.join(", ") || "keine"})`);
|
||||
/* BEIDE SEITEN GEGENEINANDER. Die Tafel bewacht die Seite, die
|
||||
Menge im Modul die Daten. Wer nur eine pflegt, baut entweder eine
|
||||
Tuer ohne Raum oder einen Raum ohne Tuer -- und beides faellt
|
||||
niemandem auf. */
|
||||
ok(JSON.stringify(ausTafel) === JSON.stringify(ausProgramm),
|
||||
`Tafel und Schnittstelle nennen dieselben Rollen (${ausProgramm.join(", ")})`,
|
||||
);
|
||||
ok(ausTafel.includes("modi") && ausTafel.includes("hand")
|
||||
&& ausTafel.includes("linke") && ausTafel.includes("admin"),
|
||||
"und es sind genau die vier aus dem Auftrag");
|
||||
}
|
||||
|
||||
const kekse = {};
|
||||
for (const [rolle] of ROLLEN) kekse[rolle] = await anmelden(rolle, CREW);
|
||||
ok(["admin", "hand", "linke", "modi"].every((r) => kekse[r]),
|
||||
"die vier melden sich auf der Team-Adresse an");
|
||||
|
||||
melde("");
|
||||
melde("=== 2. Und wer nicht ===");
|
||||
for (const rolle of ["gast", "creator"]) {
|
||||
if (!kekse[rolle]) { ok(true, `${rolle}: kommt auf dieser Adresse gar nicht herein`); continue; }
|
||||
const a = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
||||
ok(a.code === 404, `${rolle}: bekommt 404 an der Schnittstelle (${a.code})`);
|
||||
}
|
||||
{
|
||||
/* DIE ANDERE ADRESSE. DogFather arbeitet in beiden Haeusern -- und
|
||||
genau deshalb ist er hier der richtige Pruefstein: Bei ihm
|
||||
scheitert es nicht an der Rolle, sondern an der Adresse. */
|
||||
const keksAgentur = await anmelden("admin", AGENTUR);
|
||||
ok(!!keksAgentur, "DogFather meldet sich auch auf der Agenturadresse an");
|
||||
const a = await roh(AGENTUR, "/workspace/api/notizblock", { keks: keksAgentur });
|
||||
ok(a.code === 404, `und bekommt dort 404 auf seinen Block (${a.code})`);
|
||||
const s = await roh(AGENTUR, "/workspace/notizen.html", { keks: keksAgentur });
|
||||
ok(s.code === 404, `auch die Seite selbst gibt es dort nicht (${s.code})`);
|
||||
const s2 = await roh(CREW, "/workspace/notizen.html", { keks: kekse.admin });
|
||||
ok(s2.code === 200, `auf der Team-Adresse schon (${s2.code})`);
|
||||
}
|
||||
{
|
||||
const ohne = await roh(CREW, "/workspace/api/notizblock");
|
||||
ok(ohne.code === 401, `ohne Anmeldung: 401 (${ohne.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. SCHREIBEN, LESEN, ÄNDERN
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 3. Ein Blatt ===");
|
||||
let blattId = null;
|
||||
{
|
||||
const leer = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
ok(leer.code === 200 && Array.isArray(leer.j?.notizen) && leer.j.notizen.length === 0,
|
||||
`am Anfang ist der Block leer (${leer.j?.notizen?.length})`);
|
||||
ok(Array.isArray(leer.j?.farben) && leer.j.farben.length >= 4,
|
||||
`die Papierfarben kommen vom Server (${leer.j?.farben?.length})`);
|
||||
|
||||
const neu = await roh(CREW, "/workspace/api/notizblock", {
|
||||
method: "POST", keks: kekse.modi, rumpf: { text: "Erste Zeile\nZweite Zeile" },
|
||||
});
|
||||
blattId = neu.j?.notiz?.id;
|
||||
ok(neu.code === 201 && blattId > 0, `ein Blatt anlegen geht (${neu.code})`);
|
||||
ok(neu.j?.notiz?.titel === "Erste Zeile",
|
||||
`der Titel wird aus der ersten Zeile abgeleitet ("${neu.j?.notiz?.titel}")`);
|
||||
|
||||
const aend = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
||||
});
|
||||
ok(aend.code === 200 && typeof aend.j?.geaendert === "string",
|
||||
`aendern geht und gibt den Zeitstempel zurueck (${aend.code})`);
|
||||
|
||||
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
ok(nach.j?.notizen?.[0]?.text?.includes("[x] Brot"), "und der Text steht drin");
|
||||
ok(nach.j?.notizen?.[0]?.titel === "Einkauf",
|
||||
`die Raute zaehlt nicht zum Titel ("${nach.j?.notizen?.[0]?.titel}")`);
|
||||
}
|
||||
{
|
||||
const falsch = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "knallorange" },
|
||||
});
|
||||
ok(falsch.code === 400, `eine erfundene Papierfarbe wird abgelehnt (${falsch.code})`);
|
||||
const echt = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "rosa" },
|
||||
});
|
||||
ok(echt.code === 200, `eine echte nicht (${echt.code})`);
|
||||
}
|
||||
{
|
||||
const lang = "x".repeat(30000);
|
||||
const a = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { text: lang },
|
||||
});
|
||||
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
const drin = nach.j?.notizen?.find((n) => n.id === blattId)?.text || "";
|
||||
/* ABGESCHNITTEN UND NICHT ABGELEHNT. Wer zwanzigtausend Zeichen
|
||||
getippt hat, soll nicht alles verlieren, weil er hundert zu viel
|
||||
hatte -- er soll merken, dass hier Schluss ist. */
|
||||
ok(a.code === 200 && drin.length === nach.j.text_max,
|
||||
`zu langer Text wird gekuerzt statt abgelehnt (${drin.length} von ${nach.j?.text_max})`);
|
||||
await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
||||
});
|
||||
}
|
||||
{
|
||||
const fremd = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { text: "x" }, origin: "https://boese.example",
|
||||
});
|
||||
ok(fremd.code === 403, `eine Anfrage von einer fremden Seite wird abgewiesen (${fremd.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN
|
||||
|
||||
Die Zusage steht auf der Kachel. Sie ist auch die einzige, deren
|
||||
Bruch der Schreiber nicht bemerken würde.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 4. Niemand sonst ===");
|
||||
for (const rolle of ["admin", "hand", "linke"]) {
|
||||
const liste = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
||||
const fremdeDrin = (liste.j?.notizen || []).some((n) => n.id === blattId);
|
||||
ok(liste.code === 200 && !fremdeDrin,
|
||||
`${rolle}: sieht das Blatt des Modis nicht in seiner Liste`);
|
||||
const holen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse[rolle], rumpf: { text: "hier war ich" },
|
||||
});
|
||||
ok(holen.code === 404, `${rolle}: kann es auch nicht aendern (${holen.code})`);
|
||||
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "DELETE", keks: kekse[rolle],
|
||||
});
|
||||
ok(weg.code === 404, `${rolle}: und nicht wegwerfen (${weg.code})`);
|
||||
}
|
||||
{
|
||||
/* GEGENPROBE. Ohne sie wuerde die Messung oben auch dann gruen, wenn
|
||||
das Blatt gar nicht existiert. */
|
||||
const eigen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "PATCH", keks: kekse.modi, rumpf: { angeheftet: true },
|
||||
});
|
||||
ok(eigen.code === 200, `Gegenprobe: dem Modi selbst gelingt genau dasselbe (${eigen.code})`);
|
||||
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
ok(nach.j?.notizen?.[0]?.angeheftet === true, "und das Anheften kommt an");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. DER PAPIERKORB
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 5. Der Papierkorb ===");
|
||||
{
|
||||
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
||||
method: "DELETE", keks: kekse.modi,
|
||||
});
|
||||
ok(weg.code === 200, `wegtun geht (${weg.code})`);
|
||||
const block = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
ok(!(block.j?.notizen || []).some((n) => n.id === blattId), "und es ist vom Block verschwunden");
|
||||
ok(block.j?.papierkorb === 1, `der Papierkorb zaehlt es (${block.j?.papierkorb})`);
|
||||
|
||||
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
||||
const drin = (korb.j?.notizen || []).find((n) => n.id === blattId);
|
||||
ok(!!drin, "es liegt im Papierkorb");
|
||||
ok(typeof drin?.endgueltig_am === "string"
|
||||
&& Date.parse(drin.endgueltig_am) > Date.now(),
|
||||
`und es steht dabei, wann es von selbst geht (${drin?.endgueltig_am?.slice(0, 10)})`);
|
||||
|
||||
const zurueck = await roh(CREW, `/workspace/api/notizblock/${blattId}/zurueck`, {
|
||||
method: "POST", keks: kekse.modi,
|
||||
});
|
||||
ok(zurueck.code === 200, `zurueckholen geht (${zurueck.code})`);
|
||||
const block2 = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
ok((block2.j?.notizen || []).some((n) => n.id === blattId), "und es ist wieder da");
|
||||
}
|
||||
{
|
||||
/* ENDGUELTIG NUR AUS DEM PAPIERKORB. Vom Block aus gibt es diesen
|
||||
Weg nicht -- wer danebentippt, soll nicht alles verlieren. */
|
||||
const zuFrueh = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
||||
method: "DELETE", keks: kekse.modi,
|
||||
});
|
||||
ok(zuFrueh.code === 404,
|
||||
`endgueltig loeschen geht vom Block aus NICHT (${zuFrueh.code})`);
|
||||
await roh(CREW, `/workspace/api/notizblock/${blattId}`, { method: "DELETE", keks: kekse.modi });
|
||||
const jetzt = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
||||
method: "DELETE", keks: kekse.modi,
|
||||
});
|
||||
ok(jetzt.code === 200, `aus dem Papierkorb schon (${jetzt.code})`);
|
||||
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
||||
ok((korb.j?.notizen || []).length === 0, "und dann ist es wirklich weg");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. DAS AUFRÄUMEN HÄLT SEINE FRIST
|
||||
|
||||
Eine Frist ohne Werkzeug ist ein Satz in einem Kommentar. Geprüft
|
||||
wird deshalb an echten Daten, in beide Richtungen.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 6. Dreissig Tage ===");
|
||||
{
|
||||
const { notizenAufraeumen } = await import("./workspace-notizen.js");
|
||||
const { PAPIERKORB_TAGE } = await import("./notiz-tabellen.js");
|
||||
const { db } = await import("./workspace.js");
|
||||
const alt = new Date(Date.now() - (PAPIERKORB_TAGE + 2) * 86400000).toISOString();
|
||||
const frisch = new Date(Date.now() - 86400000).toISOString();
|
||||
const einfuegen = (weg) => db().prepare(
|
||||
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert, weg_am)"
|
||||
+ " VALUES (?,?,?,0,?,?,?) RETURNING id")
|
||||
.get(IDS.modi, "Probe", "block", nun, nun, weg).id;
|
||||
const idAlt = einfuegen(alt);
|
||||
const idNeu = einfuegen(frisch);
|
||||
const idOffen = db().prepare(
|
||||
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert)"
|
||||
+ " VALUES (?,?,?,0,?,?) RETURNING id").get(IDS.modi, "Auf dem Block", "block", nun, nun).id;
|
||||
|
||||
const weg = notizenAufraeumen();
|
||||
const da = (id) => !!db().prepare("SELECT 1 FROM notizen WHERE id = ?").get(id);
|
||||
ok(weg === 1, `genau eines wird geraeumt (${weg})`);
|
||||
ok(!da(idAlt), `das alte ist weg (nach ${PAPIERKORB_TAGE} Tagen)`);
|
||||
ok(da(idNeu), "das von gestern liegt noch da");
|
||||
/* DIE WICHTIGERE HAELFTE: Was auf dem Block liegt, wird NIE
|
||||
geraeumt. Ohne diese Zeile waere ein Aufraeumer, der alles
|
||||
loescht, hier gruen. */
|
||||
ok(da(idOffen), "und was gar nicht weggelegt wurde, bleibt unberuehrt");
|
||||
db().prepare("DELETE FROM notizen WHERE id IN (?,?)").run(idNeu, idOffen);
|
||||
}
|
||||
{
|
||||
/* Steht die Frist auch in der Aufbewahrungsliste? Sie ist die
|
||||
einzige Stelle im Haus, die wirklich raeumt -- am 24.09. war
|
||||
genau dieser Eintrag beim Support vergessen worden, und die
|
||||
Meldungen waeren fuer immer liegen geblieben. */
|
||||
const { FRISTEN } = await import("./workspace-aufbewahrung.js");
|
||||
const eintrag = (FRISTEN || []).find((e) => e.schluessel === "notizen_papierkorb");
|
||||
ok(!!eintrag, "der Papierkorb steht in der Aufbewahrungsliste");
|
||||
ok(typeof eintrag?.raeumen === "function",
|
||||
"und hat ein Werkzeug, das ihn wirklich raeumt");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. DAS BLATT AUF DEM BILDSCHIRM
|
||||
|
||||
Jetzt der Teil, den keine Schnittstelle beantworten kann.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 7. Das Blatt auf dem Bildschirm ===");
|
||||
|
||||
const { execFileSync } = await import("node:child_process");
|
||||
const { createServer: httpsServer } = await import("node:https");
|
||||
const { request: httpAnfrage } = await import("node:http");
|
||||
|
||||
const schluesselDatei = join(ordner, "pruef.key");
|
||||
const zertDatei = join(ordner, "pruef.crt");
|
||||
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
|
||||
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
||||
{ stdio: "ignore" });
|
||||
|
||||
const HTTPS_PORT = PORT + 1;
|
||||
const vorbau = httpsServer({
|
||||
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
|
||||
}, (von, nach) => {
|
||||
const weiter = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
||||
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
|
||||
}, (antwort) => {
|
||||
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
||||
antwort.pipe(nach);
|
||||
});
|
||||
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
|
||||
von.pipe(weiter);
|
||||
});
|
||||
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
||||
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
|
||||
|
||||
const pw = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await pw.chromium.launch({
|
||||
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`,
|
||||
"--ignore-certificate-errors"],
|
||||
});
|
||||
const kontext = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: true,
|
||||
});
|
||||
await kontext.addCookies((kekse.modi || "").split("; ").filter(Boolean).map((k) => {
|
||||
const [n, ...r] = k.split("=");
|
||||
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
||||
}));
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
|
||||
await seite.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(700);
|
||||
|
||||
ok(await seite.locator("#block").count() === 1, "die Seite kommt an");
|
||||
|
||||
/* Ein Blatt anlegen und Text hineinschreiben, der alle Auszeichnungen
|
||||
enthaelt -- und eine SEHR lange Zeile, die umbrechen MUSS. Genau an
|
||||
ihr faellt eine Breitenaenderung auf: Brechen die zwei Schichten an
|
||||
verschiedenen Stellen um, sind sie verschieden hoch. */
|
||||
const PROBE = [
|
||||
"# Eine Ueberschrift",
|
||||
"[ ] Offener Punkt",
|
||||
"[x] Erledigter Punkt",
|
||||
"- Ein Aufzaehlungspunkt",
|
||||
"> Ein Zitat",
|
||||
"Ein Link: https://example.org/pfad",
|
||||
"Eine sehr lange Zeile, die auf jeden Fall umbrechen muss, damit man sieht, "
|
||||
+ "ob beide Schichten an derselben Stelle umbrechen, und deshalb ist sie so "
|
||||
+ "lang wie sie ist, mit vielen Woertern hintereinander ohne Punkt und Komma "
|
||||
+ "damit der Umbruch wirklich mitten im Satz passiert und nicht am Ende.",
|
||||
].join("\n");
|
||||
|
||||
await seite.click("#neu");
|
||||
await seite.waitForTimeout(400);
|
||||
await seite.fill("#feld", PROBE);
|
||||
await seite.waitForTimeout(400);
|
||||
|
||||
{
|
||||
const m = await seite.evaluate(() => {
|
||||
const f = document.getElementById("feld");
|
||||
const dk = document.getElementById("deck");
|
||||
const cf = getComputedStyle(f), cd = getComputedStyle(dk);
|
||||
const gleich = (n) => cf[n] === cd[n];
|
||||
return {
|
||||
hoeheFeld: f.scrollHeight, hoeheDeck: dk.scrollHeight,
|
||||
breiteFeld: f.clientWidth, breiteDeck: dk.clientWidth,
|
||||
schriftGleich: ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
||||
"letterSpacing", "wordSpacing", "lineHeight", "whiteSpace",
|
||||
"overflowWrap", "tabSize", "paddingLeft", "paddingRight",
|
||||
"paddingTop"].filter((n) => !gleich(n)),
|
||||
/* Und dasselbe fuer JEDES ausgezeichnete Stueck im Deck: Wenn
|
||||
eine Auszeichnung die Schrift veraendert, steht es hier. */
|
||||
stuecke: [...dk.querySelectorAll("span, a")].map((s) => {
|
||||
const c = getComputedStyle(s);
|
||||
return ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
||||
"letterSpacing", "wordSpacing"]
|
||||
.filter((n) => c[n] !== cf[n])
|
||||
.map((n) => `${s.className || s.tagName}.${n}=${c[n]}`);
|
||||
}).flat(),
|
||||
haken: dk.querySelectorAll(".n-hk").length,
|
||||
hakenAn: dk.querySelectorAll('.n-hk[data-an="ja"]').length,
|
||||
links: dk.querySelectorAll("a.n-link").length,
|
||||
ueber: dk.querySelectorAll(".n-h").length,
|
||||
text: dk.textContent,
|
||||
};
|
||||
});
|
||||
|
||||
ok(m.schriftGleich.length === 0,
|
||||
`beide Schichten schreiben gleich (${m.schriftGleich.join(", ") || "alles gleich"})`);
|
||||
ok(m.stuecke.length === 0,
|
||||
`keine Auszeichnung aendert die Schrift (${m.stuecke.slice(0, 3).join(", ") || "keine"})`);
|
||||
ok(m.breiteFeld === m.breiteDeck,
|
||||
`beide sind gleich breit (${m.breiteFeld} / ${m.breiteDeck})`);
|
||||
/* DIE EIGENTLICHE MESSUNG. Zwei Bildpunkte Spielraum fuers Runden;
|
||||
ein verschobener Umbruch kostet eine ganze Zeile (30 px). */
|
||||
ok(Math.abs(m.hoeheFeld - m.hoeheDeck) <= 2,
|
||||
`und gleich hoch -- der Umbruch sitzt an derselben Stelle `
|
||||
+ `(${m.hoeheFeld} / ${m.hoeheDeck})`);
|
||||
ok(m.text.replace(/\s+$/, "") === PROBE,
|
||||
"die Deckschicht zeigt genau den Text, Zeichen fuer Zeichen");
|
||||
ok(m.haken === 2 && m.hakenAn === 1,
|
||||
`zwei Kaestchen, eines angehakt (${m.haken}/${m.hakenAn})`);
|
||||
ok(m.links === 1, `die Adresse ist anklickbar (${m.links})`);
|
||||
ok(m.ueber === 1, `die Ueberschrift ist ausgezeichnet (${m.ueber})`);
|
||||
}
|
||||
|
||||
/* GEGENPROBE ZUR HOEHENMESSUNG. Ohne sie wuerde die Zeile oben auch
|
||||
dann gruen, wenn die Messung gar nichts ansieht. Ein Fettdruck auf
|
||||
der Deckschicht MUSS sie rot machen. */
|
||||
{
|
||||
const unterschied = await seite.evaluate(() => {
|
||||
const f = document.getElementById("feld");
|
||||
const dk = document.getElementById("deck");
|
||||
const vorher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
||||
dk.style.fontWeight = "800";
|
||||
dk.style.letterSpacing = "0.4px";
|
||||
const nachher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
||||
dk.style.fontWeight = ""; dk.style.letterSpacing = "";
|
||||
return { vorher, nachher };
|
||||
});
|
||||
ok(unterschied.nachher > 2,
|
||||
`Gegenprobe: Fettdruck auf der Deckschicht bricht die Deckung `
|
||||
+ `(${unterschied.vorher} -> ${unterschied.nachher} px Unterschied)`);
|
||||
}
|
||||
|
||||
/* Ein Klick auf ein Kaestchen schaltet es um. */
|
||||
{
|
||||
const vorher = await seite.inputValue("#feld");
|
||||
await seite.locator("#deck .n-hk").first().click();
|
||||
await seite.waitForTimeout(250);
|
||||
const nachher = await seite.inputValue("#feld");
|
||||
ok(vorher.includes("[ ] Offener Punkt") && nachher.includes("[x] Offener Punkt"),
|
||||
"ein Klick auf das Kaestchen setzt den Haken");
|
||||
ok(nachher.length === vorher.length,
|
||||
`und der Text bleibt zeichengenau gleich lang (${nachher.length}/${vorher.length})`);
|
||||
await seite.locator("#deck .n-hk").first().click();
|
||||
await seite.waitForTimeout(250);
|
||||
ok((await seite.inputValue("#feld")).includes("[ ] Offener Punkt"),
|
||||
"noch ein Klick nimmt ihn wieder weg");
|
||||
}
|
||||
|
||||
/* Die Tastatur fuehrt die Liste fort. */
|
||||
{
|
||||
await seite.fill("#feld", "- Erster Punkt");
|
||||
await seite.click("#feld");
|
||||
await seite.keyboard.press("Control+End");
|
||||
await seite.keyboard.press("Enter");
|
||||
await seite.keyboard.type("Zweiter");
|
||||
await seite.waitForTimeout(200);
|
||||
const t = await seite.inputValue("#feld");
|
||||
ok(t === "- Erster Punkt\n- Zweiter",
|
||||
`Enter setzt die Aufzaehlung fort ("${t.replace(/\n/g, "\\n")}")`);
|
||||
|
||||
await seite.fill("#feld", "[x] Fertig");
|
||||
await seite.click("#feld");
|
||||
await seite.keyboard.press("Control+End");
|
||||
await seite.keyboard.press("Enter");
|
||||
await seite.keyboard.type("Noch offen");
|
||||
await seite.waitForTimeout(200);
|
||||
const t2 = await seite.inputValue("#feld");
|
||||
/* DER HAKEN WIRD NICHT MITGENOMMEN -- die naechste Aufgabe ist noch
|
||||
nicht erledigt. Das ist die Sorte Kleinigkeit, die den
|
||||
Unterschied macht zwischen „funktioniert" und „denkt mit". */
|
||||
ok(t2 === "[x] Fertig\n[ ] Noch offen",
|
||||
`und uebernimmt einen gesetzten Haken NICHT ("${t2.replace(/\n/g, "\\n")}")`);
|
||||
}
|
||||
|
||||
/* Es sichert sich von selbst -- gemessen am Neuladen, nicht an der
|
||||
Anzeige. Eine Anzeige, die „gesichert" sagt, ist eine Behauptung. */
|
||||
{
|
||||
const wort = `Selbstgesichert-${Date.now()}`;
|
||||
await seite.fill("#feld", wort);
|
||||
await seite.waitForTimeout(1400);
|
||||
const stand = (await seite.locator("#stand").textContent() || "").trim();
|
||||
/* GEFRAGT WIRD DER SERVER, nicht die Seite nach dem Neuladen.
|
||||
|
||||
Der erste Anlauf lud neu und sah ins Schreibfeld -- und wurde
|
||||
rot, obwohl alles richtig war: Die Seite schlaegt nach dem Laden
|
||||
das OBERSTE Blatt auf, und das war ein anderes (ein angeheftetes
|
||||
aus Abschnitt 4). Gemessen wurde also ein Blatt, in das nie
|
||||
jemand getippt hat. Eine Messung, die das Falsche ansieht, ist
|
||||
schlimmer als keine -- sie schickt einen los, einen Fehler zu
|
||||
suchen, den es nicht gibt. */
|
||||
const drueben = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
||||
const gefunden = (drueben.j?.notizen || []).some((n) => String(n.text).includes(wort));
|
||||
ok(gefunden, `der Text ist ohne Knopfdruck beim Server angekommen `
|
||||
+ `(Anzeige sagte "${stand}")`);
|
||||
ok(stand === "gesichert", `und die Anzeige sagt es auch ("${stand}")`);
|
||||
}
|
||||
|
||||
/* Und auf dem Handy: Das Blatt legt sich ueber die Liste, der
|
||||
Zurueck-Knopf holt sie wieder. */
|
||||
{
|
||||
const handy = await kontext.newPage();
|
||||
await handy.setViewportSize({ width: 390, height: 780 });
|
||||
await handy.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
||||
await handy.waitForTimeout(700);
|
||||
const listeDa = await handy.locator(".n-zettel").count();
|
||||
ok(listeDa >= 1, `auf dem Handy steht zuerst der Stapel (${listeDa} Blaetter)`);
|
||||
ok(await handy.locator("#blatt").isHidden(),
|
||||
"und kein Blatt liegt davor -- sonst kaeme man nie an die Liste");
|
||||
await handy.locator(".n-zettel").first().click();
|
||||
await handy.waitForTimeout(400);
|
||||
ok(await handy.locator("#blatt").isVisible(), "ein Tipp schlaegt das Blatt auf");
|
||||
ok(await handy.locator("#zurueck").isVisible(),
|
||||
"und der Zurueck-Knopf erscheint");
|
||||
/* KEIN WAAGERECHTES SCHIEBEN. Die Hausgrenze ist die Breite des
|
||||
Schirms; ein Blatt, das darueber hinausragt, macht die ganze
|
||||
Seite schiebbar. */
|
||||
const breit = await handy.evaluate(() =>
|
||||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
ok(breit <= 1, `nichts ragt seitlich hinaus (${breit} px)`);
|
||||
await handy.locator("#zurueck").click();
|
||||
await handy.waitForTimeout(400);
|
||||
ok(await handy.locator("#blatt").isHidden(), "der Zurueck-Knopf schliesst es wieder");
|
||||
await handy.close();
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
8. ZWEI WEGE MIT DEMSELBEN NAMEN -- EINE HAUSWEITE PRUEFUNG
|
||||
|
||||
Sie steht in dieser Datei, weil sie hier gefunden wurde, und sie
|
||||
gilt fuer das ganze Haus.
|
||||
|
||||
BEIM BAUEN DIESES BLOCKS: Die Route DELETE /workspace/api/notizen/:id
|
||||
gab es schon -- in workspace-aufgaben.js, fuer die Notizen AN einer
|
||||
Aufgabe. Weil aufgabenRouter frueher eingehaengt wird, hat sie jeden
|
||||
Loeschversuch des Blocks abgefangen und mit 404 beantwortet.
|
||||
|
||||
VON AUSSEN SAH DAS AUS WIE EIN FEHLER IM NEUEN MODUL: Anlegen ging,
|
||||
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und
|
||||
dort ist nichts. Express meldet so etwas nicht -- es nimmt die
|
||||
erste Route, die passt, und schweigt ueber die zweite.
|
||||
|
||||
Geprueft wird der Routenbaum von express selbst, nicht eine Liste.
|
||||
Findet sich ein Paar aus Methode und Pfad zweimal, steht hier, wo.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 8. Kein Weg heisst zweimal gleich ===");
|
||||
{
|
||||
const app = (await import("./index.js")).default;
|
||||
const wege = new Map();
|
||||
const doppelt = [];
|
||||
const sammeln = (schicht, vorn = "") => {
|
||||
if (schicht.route) {
|
||||
const pfad = vorn + (schicht.route.path || "");
|
||||
for (const m of Object.keys(schicht.route.methods || {})) {
|
||||
if (m === "_all") continue;
|
||||
const name = `${m.toUpperCase()} ${pfad}`;
|
||||
if (wege.has(name)) doppelt.push(name); else wege.set(name, true);
|
||||
}
|
||||
} else if (schicht.handle?.stack) {
|
||||
for (const s of schicht.handle.stack) sammeln(s, vorn);
|
||||
}
|
||||
};
|
||||
const stapel = app?._router?.stack || app?.router?.stack || [];
|
||||
for (const s of stapel) sammeln(s);
|
||||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende diese Messung gar keine
|
||||
Wege (weil express seinen Baum eines Tages anders ablegt), waere
|
||||
„keine Dubletten" trivial wahr -- und bliebe es fuer immer. */
|
||||
ok(wege.size > 200, `${wege.size} Wege im Baum gefunden`);
|
||||
ok(doppelt.length === 0,
|
||||
doppelt.length ? `doppelt vergeben: ${[...new Set(doppelt)].join(", ")}`
|
||||
: "kein Paar aus Methode und Pfad kommt zweimal vor");
|
||||
/* GEGENPROBE: Die Messung MUSS eine Dublette finden koennen. */
|
||||
const probe = new Map(); const gefunden = [];
|
||||
for (const n of ["GET /x", "GET /x"]) {
|
||||
if (probe.has(n)) gefunden.push(n); else probe.set(n, true);
|
||||
}
|
||||
ok(gefunden.length === 1, "Gegenprobe: zwei gleiche Wege wuerden auffallen");
|
||||
}
|
||||
|
||||
ok(konsole.length === 0, `keine Fehler in der Konsole (${konsole.slice(0, 2).join(" | ") || "keine"})`);
|
||||
|
||||
await browser.close();
|
||||
await new Promise((r) => vorbau.close(r));
|
||||
|
||||
melde("");
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
melde(fehler ? "FEHLGESCHLAGEN" : "ALLES IN ORDNUNG");
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -503,6 +503,28 @@ export const SEITEN = {
|
||||
Weiterhin ausdruecklich NICHT fuer die Agentur und nicht fuer die
|
||||
Modis: Die Seite ist eine vollstaendige Karte dieses Hauses -- jede
|
||||
Seite, jede Rolle. Wer sie hat, weiss, wo er es versuchen muesste. */
|
||||
/* DER NOTIZBLOCK (26.09.2026).
|
||||
|
||||
Filipe: „fuer die modis, rechte und linke hand und dogfather
|
||||
eine kachel hinzufuegst, sie soll: Notizen, heissen."
|
||||
|
||||
GENAU DIESE VIER, und die linke Hand steht ausdruecklich dabei
|
||||
statt sich aus der Schleife weiter unten zu ergeben: Sie ist
|
||||
hier namentlich genannt, und eine namentliche Nennung gehoert
|
||||
in die Tafel, die man liest, wenn man wissen will, wer wohin
|
||||
darf.
|
||||
|
||||
WAS AUF DER SEITE ZU SEHEN IST, entscheidet nicht diese Zeile:
|
||||
In workspace-notizen.js hat jede Abfrage `person_id = ?` fest
|
||||
eingebaut. Es gibt keine Rolle, die mehr sieht -- auch
|
||||
DogFather nicht. Diese Tafel beantwortet nur, wer die Tuer
|
||||
aufbekommt.
|
||||
|
||||
DIE AGENTUR STEHT NICHT DABEI, und die Adresse auch nicht: Die
|
||||
Seite gehoert zum Haus von Team Dogi und ist auf der
|
||||
Agenturadresse ein 404 (GEHOERT_ZU_ADRESSE in crew-adresse.js). */
|
||||
"/workspace/notizen.html": ["admin", "hand", "linke", "modi"],
|
||||
|
||||
"/workspace/rechte.html": ["admin", "hand"],
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ import express from "express";
|
||||
import { AUFBEWAHRUNG_TAGE as HILFE_TAGE, FALL_STAND } from "./hilfe-tabellen.js";
|
||||
import { AUFBEWAHRUNG_TAGE as SUPPORT_TAGE } from "./support-tabellen.js";
|
||||
import { supportAufraeumen } from "./workspace-support.js";
|
||||
import { notizenAufraeumen } from "./workspace-notizen.js";
|
||||
import { PAPIERKORB_TAGE as NOTIZ_TAGE } from "./notiz-tabellen.js";
|
||||
import { ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js";
|
||||
import { unterstuetzungAufraeumen } from "./workspace-unterstuetzung.js";
|
||||
import {
|
||||
@@ -299,6 +301,28 @@ export const FRISTEN = [
|
||||
lassen — und ein Bild ohne Zeile findet danach niemand mehr. */
|
||||
raeumen: () => supportAufraeumen(),
|
||||
},
|
||||
/* ==== DER PAPIERKORB DES NOTIZBLOCKS (26.09.2026) ==============
|
||||
|
||||
Der Eintrag entsteht ZUSAMMEN mit der Frist und nicht danach.
|
||||
Am 24.09. ist genau das schiefgegangen: `supportAufraeumen()`
|
||||
war geschrieben, exportiert und dokumentiert -- und wurde
|
||||
nirgends aufgerufen. Im Kopf der Datei stand „90 Tage", und
|
||||
geräumt wurde nie. */
|
||||
{
|
||||
schluessel: "notizen_papierkorb",
|
||||
was: "Weggelegte Notizblätter",
|
||||
art: "raeumen",
|
||||
frist: `${NOTIZ_TAGE} Tage im Papierkorb`,
|
||||
zweck: "Ein versehentlich weggetipptes Blatt zurückholen können.",
|
||||
grundlage: "Art. 5 Abs. 1 lit. e DSGVO – Speicherbegrenzung",
|
||||
wirkung: `Das Blatt verschwindet ganz. Was auf dem Block liegt, wird `
|
||||
+ `nie geräumt – nur was jemand selbst weggelegt hat, und erst `
|
||||
+ `${NOTIZ_TAGE} Tage danach.`,
|
||||
offen: (d) => zaehle(d,
|
||||
"SELECT COUNT(*) AS n FROM notizen WHERE weg_am IS NOT NULL AND weg_am < ?",
|
||||
vorTagen(NOTIZ_TAGE)),
|
||||
raeumen: () => notizenAufraeumen(),
|
||||
},
|
||||
{
|
||||
schluessel: "unterstuetzung_striche",
|
||||
was: "Tagesstriche der Unterstützen-Seite",
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/* =====================================================================
|
||||
DER NOTIZBLOCK — die Wege (26.09.2026)
|
||||
|
||||
Die Regeln und ihre Begründung stehen im Kopf von
|
||||
notiz-tabellen.js. Hier sind die Routen — und die vier Stellen, an
|
||||
denen etwas schiefgehen kann:
|
||||
|
||||
1. EINE NOTIZ DARF NIE BEIM FALSCHEN LANDEN. Jede Abfrage hat
|
||||
`person_id = ?` fest eingebaut. Es gibt keine Rolle, die mehr
|
||||
sieht, und deshalb auch keinen Zweig, der die Bedingung
|
||||
weglassen könnte. `blattFuer()` gibt den Datensatz zurück oder
|
||||
`null` — nie `true`/`false`. So kann keine Route erst „darf
|
||||
sehen" beantworten und danach trotzdem das falsche Blatt laden.
|
||||
|
||||
2. NUR AUF DER TEAM-ADRESSE. Der Block gehört zum Haus von Team
|
||||
Dogi. Auf der Agenturadresse ist er ein 404 — auch für
|
||||
DogFather, der in beiden Häusern arbeitet. Das ist dieselbe
|
||||
Trennung wie bei Chat, Kalender und Aufgaben, und sie wird hier
|
||||
geprüft und nicht nur in der Rechtetafel: Die Tafel beantwortet,
|
||||
wer die SEITE öffnen darf, nicht von welcher Adresse aus.
|
||||
|
||||
3. GESPEICHERT WIRD OHNE KNOPF. Die Oberfläche schickt beim Tippen
|
||||
alle 800 ms. Das ist viel Verkehr für wenig Inhalt — deshalb
|
||||
antwortet PATCH knapp (nur Zeitstempel), und es gibt eine
|
||||
Schreibbremse, die ein hängendes Skript stoppt, ohne einen
|
||||
schnell Tippenden zu treffen.
|
||||
|
||||
4. ENDGÜLTIG IST ENDGÜLTIG. Löschen legt ins Papierkorbfach;
|
||||
erst das zweite Löschen entfernt wirklich, und nur von dort.
|
||||
Ein direkter Weg von „auf dem Block" zu „weg" existiert nicht.
|
||||
===================================================================== */
|
||||
|
||||
import express from "express";
|
||||
import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js";
|
||||
import { PAPIERKORB_TAGE, TEXT_MAX, BLAETTER_MAX, FARBEN, FARB_NAMEN }
|
||||
from "./notiz-tabellen.js";
|
||||
|
||||
export const notizenRouter = express.Router();
|
||||
|
||||
/** Wer einen Block hat.
|
||||
*
|
||||
* Genau die vier Rollen aus Filipes Satz. Die Liste steht hier UND
|
||||
* in rechte.js, und das ist kein Versehen: Die Tafel bewacht die
|
||||
* Seite, diese Menge die Daten. Wer nur die Tafel prüfte, hätte eine
|
||||
* Schnittstelle, die jeder Angemeldete aufrufen kann — die Seite ist
|
||||
* nur HTML, die Daten sind die Sache.
|
||||
*
|
||||
* pruef-notizen hält beide gegeneinander und wird rot, wenn sie
|
||||
* auseinanderlaufen. */
|
||||
const MIT_BLOCK = new Set(["admin", "hand", "linke", "modi"]);
|
||||
|
||||
const jetzt = () => new Date().toISOString();
|
||||
|
||||
/* ---------- Schranken --------------------------------------------------- */
|
||||
|
||||
function angemeldet(req, res, next) {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
req.person = person;
|
||||
next();
|
||||
}
|
||||
|
||||
/** Team-Adresse und passende Rolle — sonst gibt es diese Seite nicht.
|
||||
*
|
||||
* 404 UND NICHT 403: Ein „verboten" verrät, dass es hier etwas gibt.
|
||||
* Auf der Agenturadresse soll der Block nicht einmal als verschlossene
|
||||
* Tür sichtbar sein. */
|
||||
function nurTeamDogi(req, res, next) {
|
||||
if (req.person?.haus !== "crew") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (!MIT_BLOCK.has(req.person?.rolle)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
next();
|
||||
}
|
||||
|
||||
/* Schutz gegen Anfragen von fremden Seiten -- wortgleich zu den
|
||||
übrigen Modulen. Fehlt der Kopf ganz (curl), ist es kein
|
||||
Browser-Angriff über eine fremde Seite; dann zählt allein das
|
||||
Cookie. */
|
||||
function gleicheHerkunft(req, res, next) {
|
||||
const herkunft = req.get("origin");
|
||||
if (!herkunft) return next();
|
||||
let erlaubt;
|
||||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||||
next();
|
||||
}
|
||||
|
||||
/* ==== DIE SCHREIBBREMSE ================================================
|
||||
|
||||
Automatisches Speichern heißt: Bei jedem Tippen kann eine Anfrage
|
||||
kommen. Die Oberfläche wartet 800 ms Ruhe ab, ein schneller
|
||||
Schreiber löst also höchstens rund 75 Anfragen je Minute aus, und
|
||||
das auch nur, wenn er die ganze Minute durchtippt.
|
||||
|
||||
DIE GRENZE IST DAS VIERFACHE DAVON. Sie steht nicht gegen den
|
||||
Menschen, sondern gegen ein Skript, das in einer Schleife hängt --
|
||||
und sie ist absichtlich so hoch, dass niemand sie im Alltag je
|
||||
sieht. Eine Bremse, die beim normalen Arbeiten greift, wird
|
||||
umgangen und ist ab da wirkungslos (siehe die Hausregel dazu).
|
||||
|
||||
Im Speicher und nicht in der Datenbank: Ein Neustart setzt sie
|
||||
zurück, und das ist richtig -- sie schützt vor einem laufenden
|
||||
Fehler, nicht vor einem Angreifer mit Geduld. */
|
||||
const SCHREIB_FENSTER_MS = 60_000;
|
||||
const SCHREIB_MAX = 300;
|
||||
const schreibZaehler = new Map(); // personId -> [Zeitpunkte]
|
||||
|
||||
function schreibBremse(req, res, next) {
|
||||
const id = req.person?.id;
|
||||
if (!id) return next();
|
||||
const jetztMs = Date.now();
|
||||
const liste = (schreibZaehler.get(id) || []).filter((t) => jetztMs - t < SCHREIB_FENSTER_MS);
|
||||
if (liste.length >= SCHREIB_MAX) {
|
||||
return res.status(429).json({
|
||||
fehler: "Zu viele Änderungen in kurzer Zeit. Warte einen Moment — "
|
||||
+ "dein Text ist nicht verloren, er wird gleich erneut gesichert.",
|
||||
});
|
||||
}
|
||||
liste.push(jetztMs);
|
||||
schreibZaehler.set(id, liste);
|
||||
/* Damit die Karte nicht ewig wächst: Wer lange nichts geschrieben
|
||||
hat, fliegt beim nächsten Durchgang heraus. */
|
||||
if (schreibZaehler.size > 500) {
|
||||
for (const [k, v] of schreibZaehler) {
|
||||
if (!v.some((t) => jetztMs - t < SCHREIB_FENSTER_MS)) schreibZaehler.delete(k);
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
notizenRouter.use("/workspace/api/notizblock", angemeldet, nurTeamDogi);
|
||||
|
||||
/* ---------- Hilfen ------------------------------------------------------ */
|
||||
|
||||
/** Ein Blatt dieser Person — oder null.
|
||||
*
|
||||
* Gibt den DATENSATZ zurück und nicht ein Ja/Nein: Eine Route, die
|
||||
* erst fragt „darf ich?" und danach selbst lädt, kann die zweite
|
||||
* Abfrage anders stellen als die erste. So kann sie es nicht. */
|
||||
function blattFuer(person, id, { ausPapierkorb = false } = {}) {
|
||||
const n = Number(id);
|
||||
if (!Number.isInteger(n) || n <= 0) return null;
|
||||
const zeile = db().prepare("SELECT * FROM notizen WHERE id = ? AND person_id = ?")
|
||||
.get(n, person.id);
|
||||
if (!zeile) return null;
|
||||
if (ausPapierkorb ? !zeile.weg_am : zeile.weg_am) return null;
|
||||
return zeile;
|
||||
}
|
||||
|
||||
/** Mehrzeiliger Text: Absätze bleiben, Leerzeilenwüsten nicht.
|
||||
*
|
||||
* Kein `trim()` am Anfang jeder Zeile — Einrückung ist in einer
|
||||
* Notiz Inhalt („ - Unterpunkt"). Entfernt werden nur Wagenrückläufe
|
||||
* (damit die Länge überall dieselbe ist) und mehr als zwei leere
|
||||
* Zeilen am Stück. */
|
||||
function textSauber(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\n{4,}/g, "\n\n\n")
|
||||
.slice(0, TEXT_MAX);
|
||||
}
|
||||
|
||||
/** Die erste nicht leere Zeile, ohne ihre Auszeichnung.
|
||||
*
|
||||
* Wird NUR für die Liste gebraucht (und für die Suche). Der Titel
|
||||
* wird nicht gespeichert: Zwei Orte für dieselbe Sache laufen
|
||||
* auseinander, sobald jemand die erste Zeile ändert und das Feld
|
||||
* nicht. Hier wird er jedes Mal aus dem Text abgeleitet und ist
|
||||
* damit immer der, der wirklich oben steht. */
|
||||
function titelVon(text) {
|
||||
const zeile = String(text || "").split("\n").find((z) => z.trim().length) || "";
|
||||
return zeile
|
||||
.replace(/^\s*#{1,3}\s*/, "")
|
||||
.replace(/^\s*[-*]\s+/, "")
|
||||
.replace(/^\s*\[[ xX]\]\s*/, "")
|
||||
.trim()
|
||||
.slice(0, 120);
|
||||
}
|
||||
|
||||
function nachDraussen(z) {
|
||||
return {
|
||||
id: z.id,
|
||||
text: z.text,
|
||||
titel: titelVon(z.text),
|
||||
farbe: z.farbe,
|
||||
angeheftet: !!z.angeheftet,
|
||||
erstellt: z.erstellt,
|
||||
geaendert: z.geaendert,
|
||||
weg_am: z.weg_am || null,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- Lesen -------------------------------------------------------- */
|
||||
|
||||
/** Der Block: alle eigenen Blätter, angeheftete zuerst. */
|
||||
notizenRouter.get("/workspace/api/notizblock", (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT * FROM notizen
|
||||
WHERE person_id = ? AND weg_am IS NULL
|
||||
ORDER BY angeheftet DESC, geaendert DESC
|
||||
LIMIT ?`).all(req.person.id, BLAETTER_MAX);
|
||||
/* DIE ZAHL KOMMT AUS DER DATENBANK, nicht aus der Liste oben: Die
|
||||
ist bei BLAETTER_MAX abgeschnitten, und eine Zählung darauf
|
||||
wäre ab dem 301. Blatt still falsch. */
|
||||
const imKorb = db().prepare(`SELECT COUNT(*) AS n FROM notizen
|
||||
WHERE person_id = ? AND weg_am IS NOT NULL`).get(req.person.id).n;
|
||||
res.json({
|
||||
notizen: zeilen.map(nachDraussen),
|
||||
papierkorb: imKorb,
|
||||
text_max: TEXT_MAX,
|
||||
blaetter_max: BLAETTER_MAX,
|
||||
papierkorb_tage: PAPIERKORB_TAGE,
|
||||
farben: FARBEN,
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Liste:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Was im Papierkorb liegt — mit dem Tag, an dem es von selbst geht. */
|
||||
notizenRouter.get("/workspace/api/notizblock/papierkorb", (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT * FROM notizen
|
||||
WHERE person_id = ? AND weg_am IS NOT NULL
|
||||
ORDER BY weg_am DESC LIMIT ?`).all(req.person.id, BLAETTER_MAX);
|
||||
res.json({
|
||||
notizen: zeilen.map((z) => ({
|
||||
...nachDraussen(z),
|
||||
/* WANN ES WIRKLICH WEG IST -- gerechnet und nicht „in 30
|
||||
Tagen" geschrieben. Wer gestern geloescht hat, will wissen,
|
||||
wie viel Zeit er noch hat, und nicht, wie lang die Frist
|
||||
ist. */
|
||||
endgueltig_am: new Date(Date.parse(z.weg_am) + PAPIERKORB_TAGE * 86400000)
|
||||
.toISOString(),
|
||||
})),
|
||||
papierkorb_tage: PAPIERKORB_TAGE,
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Papierkorb:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Schreiben ----------------------------------------------------- */
|
||||
|
||||
/** Ein neues Blatt. */
|
||||
notizenRouter.post("/workspace/api/notizblock", gleicheHerkunft, schreibBremse,
|
||||
express.json({ limit: "256kb" }), (req, res) => {
|
||||
try {
|
||||
const offen = db().prepare(`SELECT COUNT(*) AS n FROM notizen
|
||||
WHERE person_id = ? AND weg_am IS NULL`).get(req.person.id).n;
|
||||
if (offen >= BLAETTER_MAX) {
|
||||
return res.status(429).json({
|
||||
fehler: `Dein Block hat ${BLAETTER_MAX} Blätter — mehr gehen nicht. `
|
||||
+ "Räum ein paar in den Papierkorb, dann geht es weiter.",
|
||||
});
|
||||
}
|
||||
const text = textSauber(req.body?.text);
|
||||
const farbe = FARB_NAMEN.includes(req.body?.farbe) ? req.body.farbe : FARB_NAMEN[0];
|
||||
const n = jetzt();
|
||||
const { lastInsertRowid } = db().prepare(`
|
||||
INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert)
|
||||
VALUES (?,?,?,0,?,?)`).run(req.person.id, text, farbe, n, n);
|
||||
const frisch = db().prepare("SELECT * FROM notizen WHERE id = ?").get(lastInsertRowid);
|
||||
res.status(201).json({ notiz: nachDraussen(frisch) });
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Anlegen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Ändern: Text, Farbe, angeheftet. Einzeln oder zusammen.
|
||||
*
|
||||
* ANTWORTET KNAPP. Diese Route läuft beim Tippen alle 800 ms; das
|
||||
* ganze Blatt zurückzuschicken wäre bei einer langen Notiz
|
||||
* zwanzigtausend Zeichen je Tastendruckpause, für eine Information,
|
||||
* die der Browser schon hat. Zurück kommt der Zeitstempel — das
|
||||
* Einzige, was er NICHT weiß. */
|
||||
notizenRouter.patch("/workspace/api/notizblock/:id(\\d+)", gleicheHerkunft, schreibBremse,
|
||||
express.json({ limit: "256kb" }), (req, res) => {
|
||||
try {
|
||||
const z = blattFuer(req.person, req.params.id);
|
||||
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const felder = [], werte = [];
|
||||
if (req.body?.text !== undefined) {
|
||||
felder.push("text = ?"); werte.push(textSauber(req.body.text));
|
||||
}
|
||||
if (req.body?.farbe !== undefined) {
|
||||
if (!FARB_NAMEN.includes(req.body.farbe)) {
|
||||
return res.status(400).json({ fehler: "Diese Papierfarbe gibt es nicht." });
|
||||
}
|
||||
felder.push("farbe = ?"); werte.push(req.body.farbe);
|
||||
}
|
||||
if (req.body?.angeheftet !== undefined) {
|
||||
felder.push("angeheftet = ?"); werte.push(req.body.angeheftet ? 1 : 0);
|
||||
}
|
||||
if (!felder.length) return res.status(400).json({ fehler: "Nichts zu ändern." });
|
||||
|
||||
const n = jetzt();
|
||||
felder.push("geaendert = ?"); werte.push(n);
|
||||
werte.push(z.id, req.person.id);
|
||||
db().prepare(`UPDATE notizen SET ${felder.join(", ")}
|
||||
WHERE id = ? AND person_id = ?`).run(...werte);
|
||||
res.json({ geaendert: n });
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Aendern:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** In den Papierkorb. */
|
||||
notizenRouter.delete("/workspace/api/notizblock/:id(\\d+)", gleicheHerkunft, schreibBremse,
|
||||
(req, res) => {
|
||||
try {
|
||||
const z = blattFuer(req.person, req.params.id);
|
||||
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
const n = jetzt();
|
||||
db().prepare("UPDATE notizen SET weg_am = ?, angeheftet = 0 WHERE id = ? AND person_id = ?")
|
||||
.run(n, z.id, req.person.id);
|
||||
res.json({ weg_am: n, papierkorb_tage: PAPIERKORB_TAGE });
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Wegtun:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Wieder heraus. */
|
||||
notizenRouter.post("/workspace/api/notizblock/:id(\\d+)/zurueck", gleicheHerkunft, schreibBremse,
|
||||
(req, res) => {
|
||||
try {
|
||||
const z = blattFuer(req.person, req.params.id, { ausPapierkorb: true });
|
||||
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
const offen = db().prepare(`SELECT COUNT(*) AS n FROM notizen
|
||||
WHERE person_id = ? AND weg_am IS NULL`).get(req.person.id).n;
|
||||
if (offen >= BLAETTER_MAX) {
|
||||
return res.status(429).json({
|
||||
fehler: `Dein Block ist voll (${BLAETTER_MAX} Blätter). `
|
||||
+ "Tu erst eines weg, dann kommt dieses zurück.",
|
||||
});
|
||||
}
|
||||
db().prepare("UPDATE notizen SET weg_am = NULL, geaendert = ? WHERE id = ? AND person_id = ?")
|
||||
.run(jetzt(), z.id, req.person.id);
|
||||
const frisch = db().prepare("SELECT * FROM notizen WHERE id = ?").get(z.id);
|
||||
res.json({ notiz: nachDraussen(frisch) });
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Zurueck:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Endgültig — und nur aus dem Papierkorb.
|
||||
*
|
||||
* Es gibt keinen Weg von „auf dem Block" direkt zu „weg". Wer
|
||||
* danebentippt, landet im Papierkorb; erst wer dort noch einmal
|
||||
* drückt, meint es. */
|
||||
notizenRouter.delete("/workspace/api/notizblock/:id(\\d+)/endgueltig", gleicheHerkunft,
|
||||
schreibBremse, (req, res) => {
|
||||
try {
|
||||
const z = blattFuer(req.person, req.params.id, { ausPapierkorb: true });
|
||||
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
db().prepare("DELETE FROM notizen WHERE id = ? AND person_id = ?")
|
||||
.run(z.id, req.person.id);
|
||||
/* PROTOKOLLIERT WIRD NUR DAS UNWIDERRUFLICHE -- und ohne eine
|
||||
Silbe des Inhalts. Jede Textänderung zu protokollieren würde
|
||||
aus einem privaten Block eine Aktivitätsspur machen, die
|
||||
andere lesen können; genau das soll er nicht sein. Dass etwas
|
||||
endgültig verschwunden ist, gehört dagegen in die Ablage:
|
||||
Sonst sucht jemand eines Tages eine Zeile, die niemand
|
||||
vermisst hat. */
|
||||
protokolliere("notiz_endgueltig_weg", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${z.id}`,
|
||||
});
|
||||
res.json({ weg: true });
|
||||
} catch (fehler) {
|
||||
console.error("[notizen] Endgueltig:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
AUFRAEUMEN
|
||||
|
||||
Wird aus der Aufbewahrungsliste gerufen (workspace-aufbewahrung.js)
|
||||
und nirgends sonst. Das ist die einzige Stelle im Haus, die
|
||||
wirklich räumt — eine Frist ohne Eintrag dort ist kein Versprechen,
|
||||
sondern ein Satz in einem Kommentar.
|
||||
===================================================================== */
|
||||
export function notizenAufraeumen() {
|
||||
const grenze = new Date(Date.now() - PAPIERKORB_TAGE * 86400000).toISOString();
|
||||
const weg = db().prepare("DELETE FROM notizen WHERE weg_am IS NOT NULL AND weg_am < ?")
|
||||
.run(grenze);
|
||||
return Number(weg?.changes || 0);
|
||||
}
|
||||
|
||||
/* Für die Prüfung: dieselbe Menge, die `nurTeamDogi` benutzt. Sie
|
||||
wird exportiert, damit pruef-notizen sie gegen die Rechtetafel
|
||||
halten kann, statt sie abzuschreiben. */
|
||||
export const ROLLEN_MIT_BLOCK = MIT_BLOCK;
|
||||
@@ -140,6 +140,9 @@ const LANG = {
|
||||
"hilfe.html":
|
||||
"Wenn etwas vorgefallen ist, das niemand mitlesen soll. Nur DogFather "
|
||||
+ "und die rechte Hand sehen diese Meldungen.",
|
||||
"notizen.html":
|
||||
"Dein Block. Schreib hinein, was dir einfällt — es speichert sich von "
|
||||
+ "selbst, und niemand außer dir sieht es. Auch DogFather nicht.",
|
||||
"support.html":
|
||||
"Etwas an der Seite funktioniert nicht oder sieht falsch aus? Schreib "
|
||||
+ "einen Satz dazu und häng ein Bildschirmfoto an — DogFather und die "
|
||||
|
||||
@@ -34,6 +34,7 @@ import { darfSeite, OHNE_ANMELDUNG, seitenFuer } from "./rechte.js";
|
||||
import { treffTabellen, treffSperreLesen, MINDESTALTER } from "./treff-tabellen.js";
|
||||
import { hilfeTabellen } from "./hilfe-tabellen.js";
|
||||
import { supportTabellen } from "./support-tabellen.js";
|
||||
import { notizTabellen } from "./notiz-tabellen.js";
|
||||
import { unterstuetzungTabellen } from "./unterstuetzung-tabellen.js";
|
||||
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
|
||||
TEAM_DOGI_ROLLEN, CREW_ADRESSE, WORKSPACE_ADRESSE, AGENTUR_ROLLEN, BEIDE_HAEUSER_ROLLEN,
|
||||
@@ -1049,6 +1050,49 @@ const MODI_BEREICHE = [
|
||||
{ gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||||
name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle", zeichen: "steckbrief",
|
||||
ton: 10, ziel: "steckbrief.html", szene: "halle" },
|
||||
|
||||
/* =================================================================
|
||||
DER NOTIZBLOCK (26.09.2026)
|
||||
|
||||
Filipe: „fuer die modis, rechte und linke hand und dogfather
|
||||
eine kachel hinzufuegst, sie soll: Notizen, heissen. ich will
|
||||
dass du das auch wie ein notizblock erstellst."
|
||||
|
||||
GENAU DIESE VIER ROLLEN, und dafuer muss hier nichts aufgezaehlt
|
||||
werden: MODI_BEREICHE ist die Grundliste, aus der HAND_BEREICHE
|
||||
durch Erweitern entsteht und LINKE_MIT_TREFF durch Wegnehmen
|
||||
einer einzigen Kachel. Wer hier eintraegt, trifft Modi, rechte
|
||||
Hand, linke Hand und DogFather auf der Team-Adresse -- und
|
||||
sonst niemanden. Eine eigene Liste waere die, die beim naechsten
|
||||
Rollenzuschnitt auseinanderlaeuft.
|
||||
|
||||
ER GEHOERT ZU „FUER DICH" UND NICHT ZU „TAEGLICH", obwohl man
|
||||
ihn taeglich aufmacht. Die Gruppe sagt die wichtigste
|
||||
Eigenschaft, bevor man die Kachel anfasst: Diese Notizen sieht
|
||||
NIEMAND ausser dem, der sie schreibt -- auch DogFather nicht.
|
||||
Stuende die Kachel zwischen Chat und Kalender, waere die erste
|
||||
Frage „wer liest das mit?", und die Antwort stuende erst auf der
|
||||
Seite.
|
||||
|
||||
NUR AUF DER TEAM-ADRESSE. Die Seite steht in
|
||||
GEHOERT_ZU_ADRESSE (crew-adresse.js) und ist auf der
|
||||
Agenturadresse ein 404 -- auch fuer DogFather, der in beiden
|
||||
Haeusern arbeitet. Das ist dieselbe Trennung wie bei Chat,
|
||||
Kalender und Aufgaben: Auf jeder Adresse sieht er nur deren
|
||||
Bestand.
|
||||
|
||||
TON 46 UND DIE OBERFLAECHE `papier`. Der Ton ist die groesste
|
||||
freie Luecke der Palette; warum es kein Bernstein wurde, steht
|
||||
mit allen Messungen in tools/kachelton-regeln.mjs. Das
|
||||
Papiergefuehl traegt nicht die Farbe, sondern die OBERFLAECHE --
|
||||
Linien und ein perforierter Rand, so wie `silber` bei den
|
||||
Aufgaben und `regenbogen` bei Willkommen. Faellt sie aus (alter
|
||||
Zwischenspeicher, Kontrastmodus), bleibt die Kachel in ihrer
|
||||
Farbe und damit unterscheidbar. */
|
||||
{ gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||||
name: "Notizen", unter: "Dein Block – nur du siehst ihn",
|
||||
zeichen: "notizen", ton: 46, papier: true,
|
||||
ziel: "notizen.html", szene: "halle" },
|
||||
/* ==== DIE WISSENS-KACHEL IST HIER RAUS (24.09.2026) =============
|
||||
|
||||
Filipe, zur Frage, wohin die neun Dokumente gehören: „diese
|
||||
@@ -5990,6 +6034,9 @@ export function db() {
|
||||
kommt deshalb hierher, nicht in den Bauplan darueber. */
|
||||
hilfeTabellen(d);
|
||||
supportTabellen(d);
|
||||
/* Der Notizblock (26.09.2026). Haengt an personen (CASCADE) und
|
||||
kommt deshalb hierher, nicht in den Bauplan darueber. */
|
||||
notizTabellen(d);
|
||||
/* Die Wege zum Unterstützen (24.09.2026). Sie hängen an personen
|
||||
(geaendert_von) und kommen deshalb hierher, nicht in den Bauplan
|
||||
darüber. */
|
||||
|
||||
Reference in New Issue
Block a user