Aus der Probe wird ein Zugang - und die Seite sagt nicht mehr "Agentur"
Die drei offenen Punkte aus Abschnitt 9 des Plans, plus Filipes zweite
Ansage: "auf dieser seite soll nichts stehen von agentur, diese seite
hier ist nur fuer mich meine modis und meine community."
1. PROBE -> ZUGANG (ein Knopf statt eines Satzes)
Auf der Stufe "Probe" stand bisher "Zugang in Personen & Zugaenge
anlegen, Rolle Modi" - und wer das vergass, hatte eine Karte auf "Im
Team" und keinen Menschen darin. Jetzt haengt das Anlegen am Schritt
selbst.
Ueber DENSELBEN Weg wie in "Personen & Zugaenge", nicht ueber einen
zweiten: POST /workspace/api/verwaltung/personen, mit ihrer
Rechtepruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Der Talente-Server legt selbst keinen einzigen Zugang
an, und die Pruefung zaehlt nach, dass es bei einem Weg bleibt.
Der Code steht im gleichen Kasten wie dort - die Gestaltung ist aus
personen.css nach start.css umgezogen, weil talente.html sie sonst nicht
laedt. Ein Geheimnis sieht im ganzen Haus gleich aus. Er steht
ausserhalb der Liste, sonst waere er in dem Moment weg, in dem er
entsteht: wenn die Karte auf "Im Team" springt.
Die rechte Hand bekommt an dieser Stelle KEINEN Knopf, sondern einen
Satz. Ein Knopf, der ihr jedes Mal "darfst du nicht" antwortet, waere
schlechter als gar keiner - er verspricht etwas.
2. DIE EIGENE KARTE (Abschnitt "Deine Karte")
Wer beschrieben wird, darf es lesen - aber erst, wenn ALLE gesetzt
haben (sonst waere die erste Einschaetzung eine Vorgabe fuer die
zweite), und ohne Namen und ohne Anlasstext. Gemessen in beide
Richtungen: vorher nicht sichtbar, nachher sichtbar.
3. DER VORLAGENTEXT
Gebaut aus den angeklickten Merkmalen, in einem Feld zum Aendern, nicht
zum Abschicken. Ohne Merkmale steht auch keines drin.
4. "GILT FUER" SAGT DER SERVER
In bereich.js stand woertlich "Agentur" - richtig auf der
Agenturadresse, falsch auf jeder anderen. Jetzt liefert der Server
`gehoert` ("Der Treff" / "Team Dogi" / "Agentur"); gemessen mit einem
einzigen Menschen auf zwei Adressen.
PRUEFUNGEN: treff 58 -> 66, nachwuchs 69 -> 109, neue-seiten 70 -> 93.
Die Katalogseiten werden jetzt auch mit den Augen der rechten Hand
angesehen - ohne das waere "Deine Karte" nie auf einem Bildschirm
gewesen. Und pruef-neue-seiten wartet nicht mehr 900 ms, sondern bis
sich der Text nicht mehr aendert: Die Karte stand da und wurde
trotzdem als fehlend gemeldet, weil zu frueh gelesen wurde.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+191
-21
@@ -121,19 +121,72 @@ await roh("/workspace/api/treff/massnahme", { cookie: kDogi },
|
||||
|
||||
/* ETWAS AUF DEN KATALOGSEITEN. Ohne eine Person im Team ist die
|
||||
Entwicklungsseite leer, ohne einen Kandidaten der Trichter -- und
|
||||
eine leere Seite erfuellt jede Bedingung, ohne etwas zu zeigen. */
|
||||
eine leere Seite erfuellt jede Bedingung, ohne etwas zu zeigen.
|
||||
|
||||
UND ZWAR AUF MEHREREN STUFEN. Der Trichter zeigt je Stufe einen
|
||||
ANDEREN naechsten Schritt; steht nur ein Kandidat auf der ersten,
|
||||
bleiben die Knoepfe der anderen vier ungesehen. Genau dort sitzen
|
||||
seit dem 11.09. die zwei neuen: der Vorlagentext auf "Beobachtet"
|
||||
und "Ins Team holen · Zugang anlegen" auf "Probe". */
|
||||
/* EINMAL ANKLOPFEN, DAMIT ES DIE TABELLEN GIBT. Sie entstehen beim
|
||||
ersten angemeldeten Aufruf des Bereichs, nicht beim Start -- ein
|
||||
INSERT davor liefe gegen eine Tabelle, die es noch nicht gibt. */
|
||||
await roh("/workspace/api/talente/lage", { cookie: kDogi });
|
||||
const kat = await import("./workspace-entwicklungs-punkte.js");
|
||||
const talKat = await import("./workspace-talent-punkte.js");
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
let idHand = null;
|
||||
{
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-MODI-0001", salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
dd.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES ('Frida','modi',?,?,?,?,1,?)")
|
||||
.run(hash, salt, 32768,
|
||||
createHmac("sha256", schluessel).update("CODE-MODI-0001").digest("hex"), jetzt);
|
||||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||||
+ " VALUES ('talente','ruhig','Kessi aus dem Chat','Faellt seit Wochen auf.',?,?,?,?)")
|
||||
.run(jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||||
const person = (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");
|
||||
dd.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", schluessel).update(code).digest("hex"), jetzt);
|
||||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
};
|
||||
person("Frida", "modi", "CODE-MODI-0001");
|
||||
idHand = person("Rieke", "hand", "CODE-HAND-0001");
|
||||
|
||||
const talent = (titel, text) => {
|
||||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||||
+ " VALUES ('talente','ruhig',?,?,?,?,?,?)")
|
||||
.run(titel, text, jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
};
|
||||
talent("Kessi aus dem Chat", "Faellt seit Wochen auf.");
|
||||
const idBeobachtet = talent("Malin aus dem Chat", "Haelt den Chat ruhig.");
|
||||
const idProbe = talent("Tino aus dem Chat", "Laeuft seit vier Wochen mit.");
|
||||
|
||||
const stufe = dd.prepare(
|
||||
"INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id) VALUES (?,?,?,?)");
|
||||
stufe.run(idBeobachtet, "beobachtet", jetzt, idDogi);
|
||||
stufe.run(idProbe, "probe", jetzt, idDogi);
|
||||
|
||||
/* MERKMALE fuer den Vorlagentext -- er wird daraus gebaut. Ohne sie
|
||||
stuende dort der Satz fuer "noch nichts beobachtet", und die neue
|
||||
Gestaltung waere zwar sichtbar, aber nicht in ihrer laengsten
|
||||
Fassung. Gemessen werden soll die laengste. */
|
||||
const merk = dd.prepare(
|
||||
"INSERT INTO talent_merkmal (eintrag_id, schluessel, von_id, am) VALUES (?,?,?,?)");
|
||||
for (const m of talKat.TALENT_ALLE.slice(0, 3)) {
|
||||
merk.run(idBeobachtet, m.schluessel, idDogi, jetzt);
|
||||
}
|
||||
|
||||
/* DIE EIGENE KARTE DER RECHTEN HAND. Sie oeffnet sich erst, wenn
|
||||
jeder Beurteiler gesetzt hat -- ueber sie urteilt nur DogFather,
|
||||
also reicht eine Zeile je Punkt. Sechs gesetzte und der Rest offen:
|
||||
So stehen beide Zustaende nebeneinander auf dem Bildschirm. */
|
||||
const stand = dd.prepare(
|
||||
"INSERT INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)"
|
||||
+ " VALUES (?,?,?,?,?,?)");
|
||||
const stufen = ["laeuft", "waechst", "laeuft", "hakt", "laeuft", "waechst"];
|
||||
kat.ENTWICKLUNG_FREMD.slice(0, 6).forEach((p, i) => {
|
||||
stand.run(idHand, p.schluessel, idDogi, stufen[i],
|
||||
stufen[i] === "hakt" ? "Zweimal zu spaet zur Schicht." : null, jetzt);
|
||||
});
|
||||
}
|
||||
dd.close();
|
||||
|
||||
@@ -299,7 +352,16 @@ const UEBERBREIT = `(() => {
|
||||
return { schiebt, raus };
|
||||
})()`;
|
||||
|
||||
async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
/* MIT EINEM FERTIGEN KEKS STATT ueber die Anmeldemaske (11.09.2026).
|
||||
|
||||
Die rechte Hand meldet sich an der CREW-WAND an, und die haengt am
|
||||
Host-Kopf -- den kann ein Browser bei einer Navigation nicht setzen.
|
||||
Ueber die Maske auf 127.0.0.1 kommt sie also gar nicht herein.
|
||||
|
||||
Das ist kein Umgehen einer Sicherung: Angemeldet wird ueber denselben
|
||||
Weg wie sonst (/api/anmelden), nur die Maske bleibt aussen vor. Was
|
||||
die Seite danach zeigt, entscheidet weiterhin der Server. */
|
||||
async function ansehen(name, pfad, code, breite, hoehe, keks = null) {
|
||||
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
@@ -308,14 +370,39 @@ async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
});
|
||||
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
||||
|
||||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="admin"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
if (keks) {
|
||||
await kontext.addCookies(keks.split("; ").filter(Boolean).map((teil) => {
|
||||
const i = teil.indexOf("=");
|
||||
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
||||
}));
|
||||
} else {
|
||||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="admin"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
}
|
||||
|
||||
await seite.goto(`${BASIS}/workspace/${pfad}`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
/* WARTEN, BIS SICH NICHTS MEHR AENDERT -- NICHT 900 MILLISEKUNDEN.
|
||||
|
||||
Gemessen am 11.09.2026: Die eigene Karte der rechten Hand stand
|
||||
nachweislich auf dem Bildschirm (versteckt: false, Antwort 200),
|
||||
aber der Text war schon vorher gelesen. Die Pruefung meldete
|
||||
"steht nicht da" fuer etwas, das dastand -- der schlimmste
|
||||
Fehlalarm, weil man danach am richtigen Code sucht.
|
||||
|
||||
Eine groessere Zahl waere derselbe Fehler mit einer anderen Zahl.
|
||||
Die Seite laedt ihre Abschnitte nacheinander; wie lange das
|
||||
dauert, haengt an ihrer Anzahl. Deshalb eine Regel statt einer
|
||||
Rechnung: gelesen wird, wenn der Text dreimal hintereinander
|
||||
derselbe war. Die Frist darueber ist nur die Notbremse. */
|
||||
await seite.waitForFunction(() => {
|
||||
const t = document.querySelector("main")?.innerText || "";
|
||||
if (window.__standStill === t) { window.__ruhig = (window.__ruhig || 0) + 1; }
|
||||
else { window.__standStill = t; window.__ruhig = 0; }
|
||||
return (window.__ruhig || 0) >= 3;
|
||||
}, null, { timeout: 12000, polling: 200 }).catch(() => {});
|
||||
|
||||
const text = await seite.evaluate(() => (document.querySelector("main")?.innerText || "").trim());
|
||||
const punkte = await seite.evaluate(() => (document.querySelector("main")?.innerText || "")
|
||||
@@ -323,12 +410,32 @@ async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
const kontrast = await seite.evaluate(KONTRAST);
|
||||
const ueberlapp = await seite.evaluate(UEBERLAPP);
|
||||
const breit = await seite.evaluate(UEBERBREIT);
|
||||
/* DIE EIGENE KARTE IST DER EINZIGE ABSCHNITT, DER SICH SELBST
|
||||
AUFDECKT. Bleibt er zu, sieht die Seite vollkommen normal aus --
|
||||
deshalb wird hier nicht nur gelesen, was dasteht, sondern gefragt,
|
||||
was der Server dazu sagt. */
|
||||
const karte = await seite.evaluate(async () => {
|
||||
const e = document.getElementById("karte-block");
|
||||
if (!e) return { da: false };
|
||||
let antwort = null;
|
||||
try {
|
||||
const a = await fetch("/workspace/api/entwicklung/meine-karte", { credentials: "same-origin" });
|
||||
antwort = { status: a.status, hat: (await a.json()).hat_karte };
|
||||
} catch (f) { antwort = { status: -1, fehler: String(f) }; }
|
||||
return { da: true, versteckt: e.hidden, antwort };
|
||||
});
|
||||
await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true });
|
||||
await kontext.close();
|
||||
return { text, punkte, kontrast, ueberlapp, breit, konsole };
|
||||
return { text, punkte, kontrast, ueberlapp, breit, konsole, karte };
|
||||
}
|
||||
|
||||
for (const [name, pfad] of [
|
||||
const gesehen = {}; // was auf jedem Bildschirm tatsaechlich stand
|
||||
|
||||
const anm3 = await roh("/workspace/api/anmelden", {}, { rolle: "hand", code: "CODE-HAND-0001" });
|
||||
const kHand = [].concat(anm3.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||||
ok(!!kHand, "die rechte Hand ist angemeldet -- ohne sie bliebe ihre eigene Karte ungesehen");
|
||||
|
||||
for (const [name, pfad, keks] of [
|
||||
["regeln", "treff-regeln.html"],
|
||||
["rechte", "rechte.html"],
|
||||
["moderation", "treff-moderation.html"],
|
||||
@@ -338,11 +445,17 @@ for (const [name, pfad] of [
|
||||
findet andere Fehler. */
|
||||
["entwicklung", "entwicklung.html"],
|
||||
["talente", "talente.html"],
|
||||
/* UND EINMAL DIESELBE SEITE MIT ANDEREN AUGEN. "Deine Karte" gibt es
|
||||
nur fuer die, ueber die geurteilt wird -- DogFather sieht sie nie.
|
||||
Ohne diese Zeile waere der ganze Abschnitt nie auf einem Bildschirm
|
||||
gewesen. */
|
||||
["entwicklung-hand", "entwicklung.html", kHand],
|
||||
]) {
|
||||
melde("");
|
||||
melde(`=== ${pfad} ===`);
|
||||
for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) {
|
||||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe);
|
||||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe, keks);
|
||||
if (breite === 1440) { gesehen[name] = r.text; gesehen[name + ":karte"] = r.karte; }
|
||||
const wo = `${breite} px`;
|
||||
|
||||
ok(r.text.length > 400, `${wo}: die Seite ist gefüllt (${r.text.length} Zeichen)`);
|
||||
@@ -374,6 +487,63 @@ for (const [name, pfad] of [
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
WAR DAS NEUE UEBERHAUPT AUF DEM BILDSCHIRM?
|
||||
|
||||
Die Messungen darueber sagen "nichts liegt uebereinander" und "alles
|
||||
lesbar" -- und beides ist auch dann wahr, wenn das Gesuchte gar nicht
|
||||
da war. Eine leere Seite erfuellt jede dieser Bedingungen.
|
||||
|
||||
Deshalb hier die Gegenfrage, und zwar in beide Richtungen: Das Neue
|
||||
MUSS dort stehen, wo es hingehoert, und darf NICHT dort stehen, wo
|
||||
es nicht hingehoert. Nur die zweite Haelfte beweist, dass die erste
|
||||
etwas heisst.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== Stand das Neue auch wirklich da? ===");
|
||||
{
|
||||
const t = gesehen.talente || "";
|
||||
ok(/Vorlagentext/.test(t),
|
||||
"Talente: der Knopf fuer den Vorlagentext stand auf der Stufe „Beobachtet“");
|
||||
ok(/Ins Team holen/.test(t),
|
||||
"Talente: „Ins Team holen · Zugang anlegen“ stand auf der Stufe „Probe“");
|
||||
/* GEGENPROBE: Der Vorlagentext gehoert auf GENAU EINE Stufe. Stuende
|
||||
er auf jeder, waere er auf vier von fuenf eine Ablenkung -- und die
|
||||
Zeile darueber waere trotzdem gruen. */
|
||||
/* ZEILENWEISE gezaehlt und nicht als Wort: "Der Vorlagentext liegt
|
||||
bereit" ist der Satz des naechsten Schritts, der Knopf steht
|
||||
allein auf seiner Zeile. Als Wort gezaehlt waeren es zwei, und die
|
||||
Zeile haette einen Fehler gemeldet, den es nicht gibt. */
|
||||
const knoepfe = (wort) => t.split("\n").filter((z) => z.trim() === wort).length;
|
||||
ok(knoepfe("Vorlagentext") === 1,
|
||||
`und zwar genau einmal, nicht bei jedem Kandidaten (${knoepfe("Vorlagentext")})`);
|
||||
ok(t.split("Ins Team holen").length - 1 === 1,
|
||||
`der Zugangsknopf ebenso (${t.split("Ins Team holen").length - 1})`);
|
||||
|
||||
const hand = gesehen["entwicklung-hand"] || "";
|
||||
const dogi = gesehen.entwicklung || "";
|
||||
/* WENN ES SCHIEFGEHT, SOLL DIE MELDUNG SAGEN, WAS STATTDESSEN DA WAR.
|
||||
"nicht gefunden" allein laesst offen, ob die Karte fehlt oder die
|
||||
ganze Seite eine andere war. */
|
||||
const obenLinks = (hand.split("\n").map((z) => z.trim()).filter(Boolean)[0] || "(leer)");
|
||||
/* OHNE RUECKSICHT AUF GROSS UND KLEIN: innerText liefert, was der
|
||||
Browser ZEIGT -- und .e-abschnitt ist in Grossbuchstaben gesetzt.
|
||||
Im Quelltext steht "Deine Karte", auf dem Bildschirm "DEINE
|
||||
KARTE". Eine Suche mit Ruecksicht darauf meldet "steht nicht da"
|
||||
fuer etwas, das dasteht. */
|
||||
ok(/Deine Karte/i.test(hand),
|
||||
`Entwicklung: die rechte Hand sieht ihre eigene Karte (${JSON.stringify(gesehen["entwicklung-hand:karte"])} / oben: ${obenLinks})`);
|
||||
ok(!/Deine Karte/i.test(dogi),
|
||||
"und DogFather sieht sie nicht -- ueber ihn urteilt hier niemand");
|
||||
ok(hand !== dogi && hand.length > 400,
|
||||
`zwei verschiedene Bildschirme (${dogi.length} / ${hand.length} Zeichen) -- der Keks hat also gewirkt`);
|
||||
/* UND KEIN NAME EINES BEURTEILERS. Was der Server nicht ausliefert,
|
||||
kann die Seite nicht zeigen -- aber genau das ist die Behauptung,
|
||||
und Behauptungen gehoeren gemessen. */
|
||||
ok(!/Filipe/.test(hand),
|
||||
"und in ihrer Karte steht kein Name dessen, der geurteilt hat");
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
melde("");
|
||||
melde(`Bilder liegen in ${BILDER}`);
|
||||
|
||||
Reference in New Issue
Block a user