Anfragen fuer Modi: ein Fragebogen, vier Wege, eine Bruecke

Filipe, mit dem Bildschirmfoto des Bretts "Mitmachen": "ich will dass
die leute sich da anonym bei mir melden koennen ... wie ein fragebogen
wieso die person modi werden will warum sie sollte, positiv negative
sachen ... und dan soll ich annehmen ablehnen, in warteschlange stellen
oder sofort kommunizieren koennen. und wenn ich annehme dan soll diese
person automatisch zu talente kategorie weiter geleitet werden."

Seine zwei Entscheidungen auf Rueckfrage: "Vertraulich, aber nicht
namenlos" und "Du und die rechte Hand".

WAS ES GIBT

  15 Fragen in 6 Gruppen, 9 davon Pflicht. Vier davon standen nicht auf
  seinem Zettel und kommen aus der Recherche zu Moderator-Bewerbungen:
  Alter, frühere Sperren, "ein Freund bricht die Regeln - was machst
  du?" und "was kannst du nicht so gut?". Die letzte ist die
  aussagekraeftigste: Wer darauf "nichts" schreibt, hat sich gerade
  selbst beantwortet.

  Vier Wege: Reden - Warteschlange - Annehmen - Ablehnen. "Reden" steht
  vorne, weil die haeufigste richtige Antwort auf eine Bewerbung eine
  Rueckfrage ist und keine Entscheidung; stuende "Annehmen" oben, wuerde
  es geklickt.

  "Sofort kommunizieren" ist ein Satz, den sie liest. Kein Chat: Ein
  Zugang aus der Community steht ausdruecklich in keiner Chatliste
  (AUSSEN_ROLLEN). Der Satz ist damit der einzige Weg, dieser Person
  etwas zu sagen -- deshalb ist er bei "Reden" und "Ablehnen" Pflicht,
  und WELCHE Wege ihn verlangen, steht im Katalog und nicht zweimal.

  Nach "Annehmen" entsteht ein Talent auf der Stufe "Angesprochen". Die
  ersten beiden Stufen fragen, ob ueberhaupt Interesse besteht; wer sich
  selbst meldet, hat das beantwortet.

WAS DABEI HERAUSKAM, DAS NICHT GESUCHT WAR

  aufgaben.js entschied mit `p.rolle === 'modi' || p.rolle === 'hand'`,
  wer eine Katalogaufgabe bekommen kann. Diese Datei ist ohne Anmeldung
  aus dem offenen Netz lesbar -- damit stand der verborgene Zugang darin
  nachzulesen. Jetzt entscheidet es der Server und schickt ein Ja/Nein.
  Gefunden von der Wortleck-Pruefung, nicht vom Auge.

  pruef-struktur meldete teilen.html seit heute frueh als "von nirgendwo
  verlinkt" -- und lag falsch: Auf sie zeigt `share_target.action` im
  Manifest. Die Manifeste sind jetzt Verweisquelle, nicht die eine Seite
  eine Ausnahme.

  Und im Bildschirmfoto stand der Schriftzug der Buehne mitten im Text
  einer Anfrage: `color-mix(..., transparent)` mischt mit DURCHSICHTIG.
  Deckend ist `#ffffff 6%`, so wie es .t-karte macht. Dieselbe Stelle
  hat mich heute schon einmal erwischt.

GEPRUEFT

  pruef-bewerbung 77, davon 14 im Browser. Vier Gegenproben, jede trifft
  genau ihr Ziel: Bruecke ohne "genau einmal" -> 2 rot, Vertraulichkeit
  weg -> 5 rot, Nachricht-Pflicht weg -> 2 rot, Knopf ohne Rollenfrage
  -> 1 rot.

  Dazu unveraendert gruen: rollen 315, modi-verborgen 80, modi-katalog
  49, entwicklung 43, treff, bereiche-lesend, haus-seiten, alle-wege,
  rechtetafel, css-klassen, struktur, deutsche-texte, start-ansicht,
  sicht, aufgabenbrett, womit, kanaele.
This commit is contained in:
2026-09-17 15:58:28 +02:00
parent a0d9dc38e4
commit 470c65a46b
50 changed files with 3035 additions and 373 deletions
+2
View File
@@ -54,6 +54,7 @@ import { teamlageRouter } from "./workspace-teamlage.js";
import { pushRouter, pushStarten } from "./workspace-push.js";
import { chatRouter } from "./workspace-chat.js";
import { videoRouter } from "./workspace-video.js";
import { bewerbungRouter } from "./workspace-bewerbung.js";
import { leistungRouter } from "./workspace-leistung.js";
import { fruehwarnungRouter } from "./workspace-fruehwarnung.js";
import { icsRouter } from "./workspace-ics.js";
@@ -267,6 +268,7 @@ app.use(aufbewahrungRouter);
app.use(auskunftRouter);
app.use(uebernahmeRouter);
app.use(befindenRouter);
app.use(bewerbungRouter);
app.use(reportRouter);
app.use(scoutRouter);
app.use(callRouter);
+580
View File
@@ -0,0 +1,580 @@
/* =====================================================================
DIE MODI-ANFRAGE (17.09.2026)
Filipe: „ich will dass die leute sich da anonym bei mir melden können
und eine anfrage machen können für modi ... und dan soll ich annehmen
ablehnen, in warteschlange stellen oder sofort kommunizieren können.
und wenn ich annehme dan soll diese person automatisch zu talente
kategorie weiter geleitet werden."
---------------------------------------------------------------------
WAS DIESE PRÜFUNG WIRKLICH ABSICHERN MUSS
Nicht „kann man ein Formular abschicken" -- das sieht man. Sondern
die drei Zusagen, deren Bruch NIEMAND meldet:
1. VERTRAULICH. Hier stehen Alter, Schwächen und alte Sperren. Wer
das liest, entscheidet mit; wer es nicht lesen darf, darf auch
nicht erfahren, DASS es etwas gibt. Also 404, nicht 403 -- und
im Protokoll steht keine Zeile Inhalt.
2. DIE BRÜCKE TRÄGT GENAU EINMAL. „Annehmen" legt ein Talent an.
Zweimal angenommen (Doppelklick, Zurück-Taste, zwei Geräte) darf
kein zweites Talent ergeben -- sonst steht dieselbe Person
doppelt im Trichter, und das merkt man erst, wenn man sie
zweimal anspricht.
3. EINE ENTSCHEIDUNG OHNE SATZ IST KEINE. „Reden" und „Ablehnen"
verlangen eine Nachricht. Sie ist der einzige Weg, dieser Person
etwas zu sagen: Ein Zugang aus der Community steht ausdrücklich
nicht in den Chatlisten (AUSSEN_ROLLEN).
---------------------------------------------------------------------
WARUM node:http UND NICHT fetch -- siehe pruef-treff.mjs: Die
Community meldet sich an einer anderen Wand an, und welche Wand gilt,
entscheidet der Host-Kopf. `fetch` in Node setzt den selbst.
===================================================================== */
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4504, "pruef-bewerbung");
const ordner = mkdtempSync(join(tmpdir(), "ws-bewerb-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(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 }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(300_000, "pruef-bewerbung");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0, offen = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* Der dritte Ausgang: konnte nicht nachsehen. Mit Grund -- sonst weiss
man danach so wenig wie vorher. */
const unklar = (t) => { offen++; console.log(" -- " + t); };
const CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const fragen = await import("./workspace-bewerbung-fragen.js");
const rollen = await import("./workspace-bewerbung-rollen.js");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
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", schluessel).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Rieke", "hand", "CODE-HAND-0001");
anlegen("Frida", "modi", "CODE-MODI-0001");
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001");
const idBenno = anlegen("Benno", "gast", "CODE-GAST-0002");
const idCora = anlegen("Cora", "gast", "CODE-GAST-0003");
d.close();
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad,
method: methode || (koerper ? "POST" : "GET"),
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
}, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const alsJson = (r) => { try { return JSON.parse(r.text); } catch { return {}; } };
async function anmelden(rolle, code, host) {
const r = await roh("/workspace/api/anmelden", host, {},
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
const kekse = [].concat(r.kopf?.["set-cookie"] || []);
return { keks: kekse.map((c) => c.split(";")[0]).join("; "), code: r.code };
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
const kKessi = await anmelden("gast", "CODE-GAST-0001", CREW);
const kBenno = await anmelden("gast", "CODE-GAST-0002", CREW);
const kCora = await anmelden("gast", "CODE-GAST-0003", CREW);
const holen = (pfad, k, host = CREW) => roh(pfad, host, { Cookie: k.keks });
const schicken = (pfad, k, koerper, methode = "POST", host = CREW) =>
roh(pfad, host, { Cookie: k.keks }, koerper, methode);
/* Ein vollstaendig ausgefuellter Bogen -- die Pflichtliste kommt aus dem
Katalog und nicht aus einer Abschrift hier. Wer eine Pflichtfrage
ergaenzt, bekommt sie automatisch mit; eine Liste hier waere die, die
beim naechsten Mal fehlt. */
function bogen(zusatz = {}) {
const a = {};
for (const s of fragen.PFLICHT) a[s] = "Antwort auf " + s + " - lang genug fuer eine Pruefung.";
a.name = zusatz.name || "Kessi";
return { ...a, ...zusatz };
}
/* =======================================================================
1. DER KATALOG
======================================================================= */
melde("");
melde("=== 1. Der Fragebogen selbst ===");
ok(fragen.BEWERBUNG_FRAGEN.length >= 12,
`der Bogen hat ${fragen.BEWERBUNG_FRAGEN.length} Fragen`);
ok(fragen.PFLICHT.length >= 6 && fragen.PFLICHT.length < fragen.BEWERBUNG_FRAGEN.length,
`davon ${fragen.PFLICHT.length} Pflicht – und nicht alle`);
{
/* JEDE FRAGE GEHOERT IN EINE GRUPPE. Eine Frage ohne Gruppe wird von
der Oberflaeche stillschweigend nicht gezeichnet -- sie faellt
nicht auf, sie fehlt einfach. */
const bekannt = new Set(fragen.BEWERBUNG_GRUPPEN.map((g) => g.schluessel));
const heimatlos = fragen.BEWERBUNG_FRAGEN.filter((f) => !bekannt.has(f.gruppe));
ok(heimatlos.length === 0,
heimatlos.length ? `ohne Gruppe: ${heimatlos.map((f) => f.schluessel).join(", ")}`
: "jede Frage steht in einer bekannten Gruppe");
const leer = fragen.BEWERBUNG_GRUPPEN.filter(
(g) => !fragen.BEWERBUNG_FRAGEN.some((f) => f.gruppe === g.schluessel));
ok(leer.length === 0,
leer.length ? `leere Gruppen: ${leer.map((g) => g.schluessel).join(", ")}`
: "und jede Gruppe hat Fragen");
}
{
const s = fragen.BEWERBUNG_FRAGEN.map((f) => f.schluessel);
ok(new Set(s).size === s.length, "die Schlüssel sind eindeutig");
}
ok(fragen.BEWERBUNG_WEGE.length === 4,
`vier Wege: ${fragen.BEWERBUNG_WEGE.map((w) => w.name).join(" · ")}`);
ok(fragen.BEWERBUNG_WEGE[0].schluessel === "gespraech",
"„Reden“ steht vorne – die häufigste richtige Antwort ist eine Rückfrage");
ok(fragen.BEWERBUNG_WEGE.filter((w) => w.weiter === "talent").length === 1,
"genau ein Weg führt zu den Talenten");
/* =======================================================================
2. WER DEN BOGEN ÜBERHAUPT BEKOMMT
======================================================================= */
melde("");
melde("=== 2. Wer ihn bekommt ===");
{
const a = alsJson(await holen("/workspace/api/bewerbung", kKessi));
ok(a.darf === true, "die Community darf eine Anfrage stellen");
ok(Array.isArray(a.fragen) && a.fragen.length === fragen.BEWERBUNG_FRAGEN.length,
`und bekommt alle ${a.fragen?.length} Fragen mitgeliefert`);
ok(typeof a.wer_liest === "string" && a.wer_liest.length > 10,
`und erfährt vorher, wer es liest („${String(a.wer_liest).slice(0, 40)}…“)`);
}
for (const [name, k] of [["ein Modi", kModi], ["die rechte Hand", kHand], ["DogFather", kDogi]]) {
const a = alsJson(await holen("/workspace/api/bewerbung", k));
ok(a.darf === false, `${name} bekommt kein Formular – wer dabei ist, bewirbt sich nicht`);
}
/* =======================================================================
3. ABSCHICKEN
======================================================================= */
melde("");
melde("=== 3. Abschicken ===");
{
/* Eine Pflichtantwort fehlt. Der Server sagt WELCHE -- sonst sucht
man in fuenfzehn Feldern. */
const unvoll = bogen();
delete unvoll[fragen.PFLICHT[2]];
const r = await schicken("/workspace/api/bewerbung", kKessi, { antworten: unvoll });
const a = alsJson(r);
ok(r.code === 400, `eine fehlende Pflichtantwort wird abgewiesen (${r.code})`);
ok(Array.isArray(a.fehlt) && a.fehlt.includes(fragen.PFLICHT[2]),
`und der Server sagt, welche (${JSON.stringify(a.fehlt)})`);
const zahl = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true })
.prepare("SELECT COUNT(*) n FROM bewerbungen").get().n;
ok(zahl === 0, "und es liegt nichts halb Fertiges in der Datenbank");
}
{
const r = await schicken("/workspace/api/bewerbung", kKessi,
{ antworten: bogen({ name: "Kessi", warum: "Ich bin fast jeden Abend da." }) });
ok(r.code === 201, `ein vollständiger Bogen geht durch (${r.code})`);
}
{
const r = await schicken("/workspace/api/bewerbung", kKessi, { antworten: bogen() });
ok(r.code === 409, `eine zweite Anfrage geht nicht, solange die erste offen ist (${r.code})`);
}
{
/* FREMDE HERKUNFT. Ein Formular auf einer fremden Seite koennte den
Weg im Browser eines Angemeldeten ausloesen. */
const r = await roh("/workspace/api/bewerbung", CREW,
{ Cookie: kBenno.keks, Origin: "https://boese.example" }, { antworten: bogen() });
ok(r.code === 403, `aus fremder Herkunft geht nichts (${r.code})`);
const gut = await schicken("/workspace/api/bewerbung", kBenno,
{ antworten: bogen({ name: "Benno" }) });
ok(gut.code === 201,
`– und die Gegenprobe: aus dem eigenen Haus geht es sehr wohl (${gut.code})`);
}
/* =======================================================================
4. VERTRAULICH
======================================================================= */
melde("");
melde("=== 4. Wer sie lesen darf ===");
for (const [name, k] of [["ein Modi", kModi], ["die Community selbst", kKessi]]) {
const r = await holen("/workspace/api/bewerbungen", k);
ok(r.code === 404,
`${name} bekommt den Eingang nicht – und zwar 404, nicht „kein Zugriff“ (${r.code})`);
}
for (const [name, k] of [["DogFather", kDogi], ["die rechte Hand", kHand]]) {
const r = await holen("/workspace/api/bewerbungen", k);
const a = alsJson(r);
ok(r.code === 200 && a.bewerbungen?.length === 2,
`${name} sieht beide Anfragen (${r.code}, ${a.bewerbungen?.length})`);
}
{
const a = alsJson(await holen("/workspace/api/bewerbungen", kDogi));
const eine = a.bewerbungen.find((b) => b.nenn_name === "Kessi");
ok(eine?.antworten?.warum === "Ich bin fast jeden Abend da.",
"die Antworten kommen vollständig mit");
ok(typeof eine?.tage === "number",
`und dabei steht, wie lange sie schon liegt (${eine?.tage} Tage)`);
}
{
/* IM PROTOKOLL STEHT KEIN INHALT. Das Protokoll lesen mehr Leute als
die Anfrage selbst. */
const p = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true })
.prepare("SELECT aktion, detail FROM protokoll WHERE aktion LIKE 'bewerbung%'").all();
ok(p.length >= 2, `das Protokoll hält die Anfragen fest (${p.length} Zeilen)`);
const verraten = p.filter((z) => /jeden Abend|Antwort auf/.test(String(z.detail || "")));
ok(verraten.length === 0,
verraten.length ? `Inhalt im Protokoll: ${JSON.stringify(verraten[0])}`
: "aber keine Zeile Inhalt – nur die Nummer");
}
/* =======================================================================
5. ENTSCHEIDEN — und der Satz, der bei der Person ankommt
======================================================================= */
melde("");
melde("=== 5. Entscheiden ===");
const idKessiAnfrage = alsJson(await holen("/workspace/api/bewerbungen", kDogi))
.bewerbungen.find((b) => b.nenn_name === "Kessi").id;
{
const r = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kDogi,
{ weg: "gespraech", grund: "" }, "PUT");
ok(r.code === 400, `„Reden“ ohne einen Satz geht nicht (${r.code})`);
/* WARUM DAS EINE ECHTE REGEL IST und keine Gaengelei: Es gibt keinen
zweiten Weg, dieser Person etwas zu sagen. Ein Zugang aus der
Community steht nicht in den Chatlisten. */
ok(rollen.BEWERBUNG_ROLLEN.has("gast"),
"denn eine Anfrage kommt aus der Community – und die steht in keiner Chatliste");
}
{
const satz = "Wie viele Abende in der Woche schaffst du realistisch?";
const r = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kDogi,
{ weg: "gespraech", grund: satz }, "PUT");
ok(r.code === 200, `mit Satz geht es (${r.code})`);
const eigen = alsJson(await holen("/workspace/api/bewerbung", kKessi));
ok(eigen.meine?.zustand === "gespraech" && eigen.meine?.grund === satz,
"und der Satz steht auf ihrer eigenen Seite – das ist das „sofort kommunizieren“");
ok(eigen.offen === true,
"die Anfrage bleibt dabei offen – „Reden“ ist keine Entscheidung");
}
{
/* DIE RECHTE HAND ENTSCHEIDET MIT -- Filipes Antwort auf die
Rueckfrage vom 17.09.: „Du und die rechte Hand." */
const r = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kHand,
{ weg: "warteschlange", grund: "" }, "PUT");
ok(r.code === 200, `die rechte Hand darf ebenfalls entscheiden (${r.code})`);
const r2 = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kModi,
{ weg: "angenommen", grund: "" }, "PUT");
ok(r2.code === 404, `ein Modi nicht – und erfährt auch nicht, dass es sie gibt (${r2.code})`);
}
{
const r = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kDogi,
{ weg: "voellig-erfunden", grund: "x" }, "PUT");
ok(r.code === 400, `ein erfundener Weg wird abgewiesen (${r.code})`);
}
/* =======================================================================
6. DIE BRÜCKE ZU DEN TALENTEN
======================================================================= */
melde("");
melde("=== 6. Aus einer Anfrage wird ein Talent ===");
{
const vorher = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true })
.prepare("SELECT COUNT(*) n FROM eintraege WHERE bereich = 'talente'").get().n;
ok(vorher === 0, "vorher steht niemand bei den Talenten");
const r = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kDogi,
{ weg: "angenommen", grund: "Willkommen." }, "PUT");
const a = alsJson(r);
ok(r.code === 200 && a.eintrag_id > 0, `„Annehmen“ legt ein Talent an (${r.code})`);
const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
const e = db2.prepare(
"SELECT titel, bereich FROM eintraege WHERE id = ?").get(a.eintrag_id);
ok(e?.bereich === "talente" && e?.titel === "Kessi",
`und zwar unter dem Namen, den sie selbst genannt hat („${e?.titel}“)`);
const st = db2.prepare("SELECT stufe FROM talent_stufe WHERE eintrag_id = ?").get(a.eintrag_id);
/* AUF „ANGESPROCHEN" -- die ersten beiden Stufen fragen, ob ueberhaupt
Interesse besteht. Wer sich selbst meldet, hat das beantwortet. */
ok(st?.stufe === "angesprochen",
`auf der Stufe, die zur Selbstmeldung passt („${st?.stufe}“)`);
/* GENAU EINMAL. Doppelklick, Zurueck-Taste, zweites Geraet -- eine
zweite Karte im Trichter faellt erst auf, wenn man dieselbe Person
zweimal anspricht. */
const nochmal = await schicken(`/workspace/api/bewerbung/${idKessiAnfrage}/weg`, kDogi,
{ weg: "angenommen", grund: "Willkommen." }, "PUT");
const b = alsJson(nochmal);
const nachher = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true })
.prepare("SELECT COUNT(*) n FROM eintraege WHERE bereich = 'talente'").get().n;
ok(nachher === 1, `zweimal annehmen ergibt kein zweites Talent (${nachher})`);
ok(b.eintrag_id === a.eintrag_id, "sondern denselben Eintrag");
}
{
const eigen = alsJson(await holen("/workspace/api/bewerbung", kKessi));
ok(eigen.meine?.zustand === "angenommen" && eigen.offen === false,
"und sie sieht auf ihrer Seite, dass sie dabei ist");
}
/* =======================================================================
7. ABLEHNEN — mit Grund, und danach eine Frist
======================================================================= */
melde("");
melde("=== 7. Ablehnen ===");
const idBennoAnfrage = alsJson(await holen("/workspace/api/bewerbungen", kDogi))
.bewerbungen.find((b) => b.nenn_name === "Benno").id;
{
const r = await schicken(`/workspace/api/bewerbung/${idBennoAnfrage}/weg`, kDogi,
{ weg: "abgelehnt", grund: "nein" }, "PUT");
ok(r.code === 400, `eine Ablehnung ohne Satz geht nicht (${r.code})`);
const r2 = await schicken(`/workspace/api/bewerbung/${idBennoAnfrage}/weg`, kDogi,
{ weg: "abgelehnt", grund: "Gerade zu wenig Zeit – melde dich im Frühjahr nochmal." }, "PUT");
ok(r2.code === 200, `mit Satz geht es (${r2.code})`);
const eigen = alsJson(await holen("/workspace/api/bewerbung", kBenno));
ok(String(eigen.meine?.grund || "").includes("Frühjahr"),
"und der Satz steht auf seiner Seite – nicht nur in der Akte");
ok(eigen.sperre?.tage > 0,
`danach läuft eine Frist (${eigen.sperre?.tage} von ${eigen.sperre?.gesamt} Tagen)`);
const neu = await schicken("/workspace/api/bewerbung", kBenno, { antworten: bogen({ name: "Benno" }) });
ok(neu.code === 429, `und eine neue Anfrage geht in dieser Frist nicht (${neu.code})`);
ok(/Tagen/.test(alsJson(neu).fehler || ""),
`die Absage sagt, WANN es wieder geht („${String(alsJson(neu).fehler).slice(0, 44)}…“)`);
}
{
/* DIE GEGENPROBE ZUR FRIST: Wer nie abgelehnt wurde, kommt durch.
Sonst hiesse „429" nur, dass gerade niemand etwas schicken kann. */
const r = await schicken("/workspace/api/bewerbung", kCora, { antworten: bogen({ name: "Cora" }) });
ok(r.code === 201, `wer nie abgelehnt wurde, kann schicken (${r.code})`);
}
/* =======================================================================
8. DIE WEGE DORTHIN
======================================================================= */
melde("");
melde("=== 8. Wie man hinkommt ===");
{
/* Der Knopf auf dem Brett „Mitmachen" -- vom Server, nicht aus einer
Datei, die jeder herunterladen kann. */
const a = alsJson(await holen("/workspace/api/bereich/mitmachen", kCora));
ok(a.weiter?.ziel === "bewerben.html",
"auf „Mitmachen“ steht für die Community der Weg zur Anfrage");
const b = alsJson(await holen("/workspace/api/bereich/mitmachen", kModi));
ok(!b.weiter,
"ein Modi sieht ihn nicht – ein Knopf, den der Server ablehnt, wäre schlimmer als keiner");
const c = alsJson(await holen("/workspace/api/bereich/regeln", kCora));
ok(!c.weiter, "und auf den anderen Brettern steht er auch nicht");
}
{
const a = alsJson(await holen("/workspace/api/bewerbungen-zahl", kDogi));
ok(a.offen === 1 && a.neu === 1,
`die Talente-Seite bekommt nur die Zahl (offen ${a.offen}, neu ${a.neu})`);
const r = await holen("/workspace/api/bewerbungen-zahl", kModi);
ok(r.code === 404, `und auch die nur die beiden (${r.code})`);
}
{
/* DIE SEITEN SELBST. Sie haben keine Kachel -- also muss die
Adressregel sie kennen, sonst landet man auf der Startseite. */
for (const [name, k, seite, erwartet] of [
["die Community", kKessi, "bewerben.html", 200],
["ein Modi", kModi, "bewerben.html", 302],
["DogFather", kDogi, "bewerbungen.html", 200],
["die rechte Hand", kHand, "bewerbungen.html", 200],
["ein Modi", kModi, "bewerbungen.html", 302],
["die Community", kKessi, "bewerbungen.html", 302],
]) {
const r = await holen("/workspace/" + seite, k);
ok(r.code === erwartet, `${name} → ${seite}: ${r.code}`);
}
/* AUCH AUF DER AGENTURADRESSE -- DogFather arbeitet auf beiden. */
const r = await roh("/workspace/bewerbungen.html", HAUS, { Cookie: kDogi.keks });
ok(r.code === 200 || r.code === 302,
`auf workspace. antwortet die Seite ebenfalls (${r.code})`);
}
/* =======================================================================
9. DIE OBERFLÄCHE
======================================================================= */
melde("");
melde("=== 9. Im echten Browser ===");
{
let chromium;
try {
({ chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"));
} catch { chromium = null; }
if (!chromium) {
unklar("kein Playwright gefunden – die Oberfläche wurde nicht angesehen");
} else {
const browser = await chromium.launch();
try {
const sicht = await browser.newContext({ viewport: { width: 390, height: 844 } });
const kekse = (k) => k.keks.split("; ").map((c) => {
const [name, ...rest] = c.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
});
/* ---- Der Fragebogen ---- */
await sicht.addCookies(kekse(kCora));
const s1 = await sicht.newPage();
const konsole = [];
s1.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
await s1.goto(`http://127.0.0.1:${PORT}/workspace/bewerben.html`,
{ waitUntil: "domcontentloaded" });
await s1.waitForTimeout(900);
const f = await s1.evaluate(() => ({
stand: document.getElementById('stand')?.textContent?.trim() || '',
bogenDa: !document.getElementById('bogen')?.hidden,
felder: document.querySelectorAll('.bw-feld__eingabe').length,
pflicht: document.querySelectorAll('.bw-feld__pflicht').length,
gruppen: document.querySelectorAll('.bw-gruppe').length,
}));
/* Cora hat gerade geschickt -- sie sieht den Stand, nicht das
Formular. Das ist die richtige Antwort und zugleich der Beweis,
dass die Seite den Zustand liest. */
ok(f.bogenDa === false && /liegt vor/.test(f.stand),
`wer schon geschickt hat, sieht den Stand statt des Formulars („${f.stand.slice(0, 32)}…“)`);
ok(konsole.length === 0,
konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "ohne Fehler in der Konsole");
await s1.close();
await sicht.close();
/* Ein unverbrauchter Zugang, um das Formular selbst zu sehen. */
const idDina = (() => {
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-GAST-0004", 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("Dina", "gast", hash, salt, 32768,
createHmac("sha256", schluessel).update("CODE-GAST-0004").digest("hex"), jetzt);
const id = dd.prepare("SELECT last_insert_rowid() AS id").get().id;
dd.close();
return id;
})();
const kDina = await anmelden("gast", "CODE-GAST-0004", CREW);
const sicht2 = await browser.newContext({ viewport: { width: 390, height: 844 } });
await sicht2.addCookies(kekse(kDina));
const s2 = await sicht2.newPage();
await s2.goto(`http://127.0.0.1:${PORT}/workspace/bewerben.html`,
{ waitUntil: "domcontentloaded" });
await s2.waitForTimeout(900);
const g = await s2.evaluate(() => ({
felder: document.querySelectorAll('.bw-feld__eingabe').length,
pflicht: document.querySelectorAll('.bw-feld__pflicht').length,
gruppen: document.querySelectorAll('.bw-gruppe').length,
werLiest: document.getElementById('wer-liest')?.textContent || '',
}));
ok(g.felder === fragen.BEWERBUNG_FRAGEN.length,
`der Bogen zeichnet alle ${g.felder} Fragen`);
ok(g.pflicht === fragen.PFLICHT.length,
`und markiert genau die ${g.pflicht} Pflichtfragen`);
ok(g.gruppen === fragen.BEWERBUNG_GRUPPEN.length,
`in ${g.gruppen} Abschnitten`);
ok(/DogFather/.test(g.werLiest),
"über dem Absenden steht, wer es liest");
/* ABSCHICKEN OHNE PFLICHTANTWORT -- die Seite markiert, sie
schickt nicht. */
await s2.click('#abschicken');
await s2.waitForTimeout(400);
const h = await s2.evaluate(() => ({
markiert: document.querySelectorAll('.bw-feld--fehlt').length,
meldung: document.getElementById('fehler')?.textContent || '',
}));
ok(h.markiert === fragen.PFLICHT.length,
`ein leerer Bogen markiert die ${h.markiert} fehlenden Felder`);
ok(/fehlen|fehlt/.test(h.meldung),
`und sagt es in Worten („${h.meldung.slice(0, 40)}“)`);
const zahlNachher = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true })
.prepare("SELECT COUNT(*) n FROM bewerbungen WHERE person_id = ?").get(idDina).n;
ok(zahlNachher === 0, "und es wurde nichts geschickt");
if (process.env.SCHIRM) {
await s2.screenshot({ path: "server/pruef-bewerbung-bogen.png", fullPage: true });
melde(" Bild: server/pruef-bewerbung-bogen.png");
}
await s2.close();
await sicht2.close();
/* ---- Der Eingang ---- */
const sicht3 = await browser.newContext({ viewport: { width: 1280, height: 900 } });
await sicht3.addCookies(kekse(kDogi));
const s3 = await sicht3.newPage();
const konsole3 = [];
s3.on("console", (m) => { if (m.type() === "error") konsole3.push(m.text()); });
await s3.goto(`http://127.0.0.1:${PORT}/workspace/bewerbungen.html`,
{ waitUntil: "domcontentloaded" });
await s3.waitForTimeout(900);
const e = await s3.evaluate(() => ({
karten: document.querySelectorAll('.bw-karte').length,
wege: document.querySelectorAll('.bw-karte .bw-weg').length,
zahlen: document.querySelectorAll('#zahlen .bilanz-zahl').length,
antworten: document.querySelectorAll('.bw-antwort').length,
}));
ok(e.karten === 3, `der Eingang zeigt alle drei Anfragen (${e.karten})`);
ok(e.wege >= 12, `mit vier Wegen je Karte (${e.wege} Knöpfe)`);
ok(e.zahlen === 3, `und drei Zahlen oben (${e.zahlen})`);
ok(e.antworten > 10, `die Antworten stehen darin (${e.antworten} Zeilen)`);
ok(konsole3.length === 0,
konsole3.length ? `Fehler in der Konsole: ${konsole3[0]}` : "ohne Fehler in der Konsole");
if (process.env.SCHIRM) {
await s3.screenshot({ path: "server/pruef-bewerbung.png", fullPage: true });
melde(" Bild: server/pruef-bewerbung.png");
}
await s3.close();
await sicht3.close();
} finally {
await browser.close();
}
}
}
melde("");
melde(`${geprueft} geprüft, ${fehler} Fehler`
+ (offen ? `, ${offen} konnte(n) nicht nachgesehen werden` : ""));
process.exit(fehler ? 1 : 0);
+14
View File
@@ -208,6 +208,20 @@ const zeigerQuellen = [
{ name: "server/workspace.js", pfad: join(SERVER, "workspace.js") },
/* Die Zugangswaende -- crew-index.html wird dort ausgeliefert. */
{ name: "server/crew-adresse.js", pfad: join(SERVER, "crew-adresse.js") },
/* EIN TEILEN-ZIEL IST EIN VERWEIS (17.09.2026).
teilen.html stand hier als "von nirgendwo verlinkt" -- und das war
falsch: Auf sie zeigt der Eintrag `share_target.action` im
Manifest. Genau dafuer gibt es die Seite; einen Verweis im HTML
hat sie absichtlich nicht, weil man sie nicht aufsucht, sondern
von TikTok aus dort landet.
Die Manifeste kommen deshalb als Quelle dazu -- nicht die eine
Seite als Ausnahme. Eine Ausnahmeliste haette beim naechsten
Teilen-Ziel wieder gefehlt. */
...["app.webmanifest", "crew.webmanifest"]
.map((n) => ({ name: n, pfad: join(WS, n) }))
.filter((q) => existsSync(q.pfad)),
];
const verlinkt = new Set(["index.html", "start.html"]);
for (const { pfad: quelle } of zeigerQuellen) {
+24
View File
@@ -281,6 +281,20 @@ export const SEITEN = {
Stufen und die Bedingungen des Ausschlusses. */
"/workspace/treff-regeln.html": ["gast", "modi", "hand", "admin"],
/* DIE ANFRAGE FUER MODI (17.09.2026).
NUR "gast" -- und das ist keine Schlamperei, sondern die Aussage.
Wer schon im Team ist, hat hier nichts zu beantworten; und ein
Formular, das auch Modis sehen, waere die Einladung, es als
Beurteilungsbogen zu benutzen.
Der Server sagt dasselbe noch einmal (SCHICKEN_ROLLEN). Zwei
Schloesser an derselben Tuer, weil die Tafel umstellbar ist:
Wer sie versehentlich oeffnet, bekommt trotzdem keine Anfrage
von einem Modi in den Eingang. */
"/workspace/bewerben.html": ["gast"],
/* DIE ENTWICKLUNGSKARTE (11.09.2026).
DogFather und die rechte Hand fuehren sie ueber das Team. Ein Modi
@@ -315,6 +329,16 @@ export const SEITEN = {
hat ein Recht darauf, dass der Kreis klein bleibt. */
"/workspace/talente.html": ["admin", "hand"],
/* DER EINGANG FUER DIE ANFRAGEN -- dieselben zwei wie bei den
Talenten, und aus demselben Grund: Hier stehen Saetze, die
jemand ueber sich selbst geschrieben hat, im Vertrauen darauf,
dass zwei Menschen sie lesen.
Filipes Entscheidung vom 17.09.2026 auf die Rueckfrage, wer
entscheiden darf: "Du und die rechte Hand." */
"/workspace/bewerbungen.html": ["admin", "hand"],
/* MELDUNGEN & MASSNAHMEN. Nur wer moderiert -- ausdruecklich OHNE
"gast": Dort stehen die Namen derer, die gemeldet haben. Eine
Meldung, die der Gemeldete lesen kann, ist keine Meldung mehr,
+11
View File
@@ -405,6 +405,17 @@ aufgabenRouter.get("/workspace/api/personen", (req, res) => {
const mitBild = (z) => ({
id: z.id, name: z.name, rolle: z.rolle,
gruppe: ROLLEN_GRUPPE[z.rolle] || "Weitere",
/* WER AUFGABEN AUS DEM KATALOG BEKOMMEN KANN (17.09.2026).
Der Browser hatte das selbst entschieden --
`p.rolle === 'modi' || p.rolle === 'hand'`, mitten in
aufgaben.js. Diese Datei kann jeder ohne Anmeldung aus dem
offenen Netz lesen; damit stand dort schwarz auf weiss, wie
der verborgene Zugang heisst.
Jetzt entscheidet es der Server und schickt ein Ja/Nein. Aus
einem "true" laesst sich nichts ablesen. */
fuer_katalog: TEAM_DOGI_ROLLEN.has(z.rolle),
bild: z.bild ? `/workspace/api/steckbrief/bild/${z.bild}` : null,
});
const ids = sichtbarePersonenIds(req.person);
+33 -1
View File
@@ -46,6 +46,10 @@ import {
darfSchreiben, freigabeBedingung, entfernenVorbereiten, TREFF_TEAM_ROLLEN,
TREFF_FREIGABE_BRETTER, mitRollenname,
} from "./workspace-treff.js";
/* Wer eine Anfrage stellen darf -- dieselbe Menge, die der
Bewerbungsweg selbst prueft. Eine zweite hier waere die, die beim
naechsten Nachschaerfen auseinanderlaeuft. */
import { BEWERBUNG_ROLLEN } from "./workspace-bewerbung-rollen.js";
export const bereicheRouter = express.Router();
@@ -1324,8 +1328,36 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
: (req.sicht || req.person)?.haus === "crew" ? "Team Dogi"
: "Agentur";
/* DER NAECHSTE SCHRITT -- vom Server, nicht vom Browser (17.09.2026).
Auf dem Brett "Mitmachen" steht, WIE man dazugehoeren kann. Was
fehlte, war die Stelle, an der man es dann auch tut: Filipe,
mit dem Bildschirmfoto dieses Bretts, "ich will dass die leute
sich da anonym bei mir melden koennen".
WARUM DER SERVER DEN KNOPF ENTSCHEIDET und nicht bereich.js:
Weil die Frage "darf diese Person?" an der Rolle haengt, und
Rollennamen stehen in keiner Datei, die jeder herunterlaedt --
bereich.js kann ohne Anmeldung aus dem offenen Netz gelesen
werden (gemessen am 17.09.). Der Browser bekommt hier einen
fertigen Satz oder gar nichts.
UND EIN KNOPF, DEN DER SERVER ABLEHNT, WAERE SCHLIMMER ALS
KEINER. Wer im Team ist, sieht ihn deshalb nicht. */
const weiter = (bereich === "mitmachen"
&& BEWERBUNG_ROLLEN.has((req.sicht || req.person)?.rolle))
? {
titel: "Du willst Modi werden?",
text: "Ein paar Fragen, ehrlich beantwortet. Es liest niemand "
+ "ausser DogFather und der rechten Hand – kein anderes "
+ "Mitglied, kein Modi.",
knopf: "Anfrage stellen",
ziel: "bewerben.html",
}
: null;
res.json({
bereich, einstellung, treff: treffBrett, gehoert,
bereich, einstellung, treff: treffBrett, gehoert, weiter,
eintraege: treffBrett ? eintraege.map(mitRollenname) : eintraege,
});
} catch (fehler) {
+300
View File
@@ -0,0 +1,300 @@
/* =====================================================================
DER FRAGEBOGEN FÜR EINE MODI-ANFRAGE (17.09.2026)
Filipe: „ich will dass die leute sich da anonym bei mir melden
können und eine anfrage machen können für modi. ich will dass es
aber profissionel gemacht ist bitte wie ein fragebogen wieso die
person modi werden will warum sie sollte, positiv negative
sachen … perfektioniere es auch mit sachen an die ich nicht denke."
---------------------------------------------------------------------
WAS „ANONYM" HIER HEISST (Filipes Entscheidung, 17.09.2026)
Vertraulich, aber nicht namenlos. Wer eine Anfrage schickt, ist
angemeldet — sonst wäre das Formular ein Briefkasten für Spam und
Scherzbewerbungen, und es gäbe keinen Weg zurückzuschreiben.
Aber: Die Anfrage erscheint NIRGENDS öffentlich. Kein anderes
Community-Mitglied sieht sie, kein Modi sieht sie. Nur DogFather
und die rechte Hand. Und wie jemand genannt werden will, entscheidet
er selbst — der Klarname wird nie verlangt.
---------------------------------------------------------------------
DIE VIER FRAGEN, AN DIE MAN NICHT DENKT
Filipe hat nach „positiv negative sachen" gefragt und ausdrücklich
um das gebeten, was ihm selbst nicht einfällt. Recherchiert
(Jotform, Typeform, Paperform, BlockSurvey — Vorlagen für Twitch-
und Discord-Moderatoren) und um vier Fragen ergänzt, die in fast
jeder guten Vorlage stehen und in fast keiner selbstgebauten:
1. ALTER. Steht nicht aus Neugier da. Für manches braucht es
Volljährigkeit (Meldungen an Plattformen, heikle Vorfälle), und
es steht bereits als Frage im Brett „Mitmachen". Wer es erst
nach der Zusage erfährt, muss sie zurücknehmen.
2. SCHON MAL GESPERRT? Die unbequemste Frage und die
aufschlussreichste. Nicht, weil eine alte Sperre
disqualifiziert — sondern weil die Antwort zeigt, wie jemand
mit eigenen Fehlern umgeht. Wer sie verschweigt und es kommt
später heraus, hat ein größeres Problem als die Sperre.
3. WAS, WENN EIN FREUND DIE REGELN BRICHT? Der häufigste echte
Konflikt im Moderationsalltag, und der, an dem die meisten
scheitern. In keiner Bewerbung, die jemand selbst schreibt,
kommt er vor.
4. WAS KANNST DU NICHT SO GUT? Filipes „negative sachen" — aber
als Frage an die Person selbst, nicht als Urteil über sie. Wer
darauf „nichts" antwortet, hat die Frage nicht verstanden, und
auch das ist eine Antwort.
---------------------------------------------------------------------
WIE DIE FRAGEN GESCHRIEBEN SIND
Für Zuschauer, die von TikTok kommen — überwiegend jung, auf dem
Handy. Also: kurze Fragen, „du", keine Behördensprache, und zu
jeder Frage ein Satz, WARUM sie gestellt wird. Ein Fragebogen, der
nicht sagt, wozu er etwas wissen will, fühlt sich wie ein Verhör an.
`pflicht: false` bei allem, was niemanden ausschließen darf. Eine
Pflichtfrage zu viel, und jemand bricht ab, den man gebraucht hätte.
===================================================================== */
/** Wie lang eine Antwort höchstens sein darf.
*
* Nicht als Schikane, sondern weil ein Feld ohne Grenze irgendwann
* eine Datenbank mit einem Roman darin ist. 1200 Zeichen sind rund
* zwei Bildschirme auf dem Handy -- mehr schreibt niemand freiwillig,
* und wer mehr zu sagen hat, sagt es im Gespräch danach. */
export const ANTWORT_MAX = 1200;
export const KURZ_MAX = 120;
export const BEWERBUNG_FRAGEN = [
/* ---- Wer bist du? ------------------------------------------------- */
{
schluessel: "name",
gruppe: "wer",
titel: "Wie sollen wir dich nennen?",
hinweis: "Dein Name im Chat reicht. Deinen echten Namen brauchen wir nicht.",
art: "kurz",
pflicht: true,
},
{
schluessel: "alter",
gruppe: "wer",
titel: "Wie alt bist du?",
/* DIE BEGRÜNDUNG STEHT DABEI. Eine Altersfrage ohne Grund liest
sich wie eine Hürde; mit Grund wie eine Notwendigkeit. */
hinweis: "Nicht aus Neugier: Für manches braucht es 18, für das meiste nicht. "
+ "Sag es einfach dazu, dann sagen wir dir, was geht.",
art: "kurz",
pflicht: true,
},
{
schluessel: "dabei_seit",
gruppe: "wer",
titel: "Seit wann schaust du DogFather – und wo?",
hinweis: "Stream, DogFather Clips, HasiDog, Discord. Ungefähr reicht.",
art: "kurz",
pflicht: false,
},
/* ---- Warum? ------------------------------------------------------- */
{
schluessel: "warum",
gruppe: "warum",
titel: "Warum möchtest du moderieren?",
hinweis: "Ehrlich ist besser als beeindruckend. „Ich bin sowieso immer da und "
+ "es nervt mich, wenn niemand eingreift“ ist eine gute Antwort.",
art: "lang",
pflicht: true,
},
{
schluessel: "warum_du",
gruppe: "warum",
titel: "Warum gerade du?",
hinweis: "Was bringst du mit, das nicht jeder mitbringt? Das muss nichts "
+ "Großes sein – Geduld zählt mehr als Erfahrung.",
art: "lang",
pflicht: true,
},
{
schluessel: "staerke",
gruppe: "warum",
titel: "Was kannst du besonders gut?",
hinweis: "Ruhig bleiben, schreiben, schlichten, Technik, Clips schneiden, "
+ "Leute willkommen heißen – irgendetwas ist da.",
art: "lang",
pflicht: false,
},
{
schluessel: "schwaeche",
gruppe: "warum",
titel: "Und was kannst du nicht so gut?",
/* DIE FRAGE, DIE SICH NIEMAND SELBST STELLT. Wer „nichts“
antwortet, hat sie nicht verstanden -- und auch das ist eine
Antwort. */
hinweis: "Jeder hat etwas. Wer hier „nichts“ schreibt, macht es sich zu leicht – "
+ "und wir merken es sowieso in der ersten Woche.",
art: "lang",
pflicht: true,
},
/* ---- Zeit --------------------------------------------------------- */
{
schluessel: "zeit",
gruppe: "zeit",
titel: "Wie viel Zeit hast du realistisch in der Woche?",
hinweis: "Zwei Stunden reichen. Verlässlich schlägt viel: Eine Schicht, auf die "
+ "man sich verlassen kann, ist mehr wert als fünf spontane.",
art: "kurz",
pflicht: true,
},
{
schluessel: "wann",
gruppe: "zeit",
titel: "Wann kannst du meistens?",
hinweis: "Abends, am Wochenende, tagsüber? Danach richten sich die Schichten.",
art: "kurz",
pflicht: false,
},
{
schluessel: "womit",
gruppe: "zeit",
titel: "Was würdest du am liebsten machen?",
hinweis: "Chat moderieren, Clips schneiden, Community betreuen, Technik – "
+ "oder etwas ganz anderes.",
art: "lang",
pflicht: false,
},
/* ---- Erfahrung und Ehrlichkeit ------------------------------------ */
{
schluessel: "erfahrung",
gruppe: "erfahrung",
titel: "Hast du schon mal moderiert?",
hinweis: "Wo, wie lange, wie groß? Wenn nein: auch völlig in Ordnung – "
+ "fast alle im Team haben ohne Erfahrung angefangen.",
art: "lang",
pflicht: false,
},
{
schluessel: "gesperrt",
gruppe: "erfahrung",
titel: "Wurdest du schon einmal irgendwo gesperrt oder verwarnt?",
/* DIE UNBEQUEMSTE FRAGE -- und die aufschlussreichste. Es geht
nicht um die Sperre, sondern um den Umgang damit. */
hinweis: "Wenn ja: wo und warum? Das ist kein Ausschlussgrund – wirklich nicht. "
+ "Aber wenn es später herauskommt und hier stand „nein“, ist es einer.",
art: "lang",
pflicht: true,
},
/* ---- Zwei Situationen --------------------------------------------- */
{
schluessel: "fall_beleidigung",
gruppe: "situation",
titel: "Jemand beleidigt DogFather im Chat, mitten im Stream. Was machst du?",
hinweis: "Es gibt keine Musterlösung. Uns interessiert, wie du denkst.",
art: "lang",
pflicht: true,
},
{
schluessel: "fall_freund",
gruppe: "situation",
titel: "Ein Freund von dir bricht die Regeln. Was machst du?",
/* DER HÄUFIGSTE ECHTE KONFLIKT -- und der, an dem die meisten
scheitern. In keiner selbst geschriebenen Bewerbung kommt er
vor. */
hinweis: "Die Frage kommt im Alltag öfter, als man denkt.",
art: "lang",
pflicht: true,
},
/* ---- Zum Schluss --------------------------------------------------- */
{
schluessel: "sonst",
gruppe: "schluss",
titel: "Gibt es sonst etwas, das wir wissen sollten?",
hinweis: "Alles, was oben nicht gepasst hat. Darf auch leer bleiben.",
art: "lang",
pflicht: false,
},
];
/** Die Gruppen, in der Reihenfolge, in der sie im Formular stehen. */
export const BEWERBUNG_GRUPPEN = [
{ schluessel: "wer", name: "Wer bist du?",
text: "Drei kurze Angaben. Deinen echten Namen brauchen wir nicht." },
{ schluessel: "warum", name: "Warum?",
text: "Der wichtigste Teil. Nimm dir zwei Minuten." },
{ schluessel: "zeit", name: "Zeit",
text: "Damit wir wissen, was realistisch ist – und dich nicht überfordern." },
{ schluessel: "erfahrung", name: "Erfahrung",
text: "Ohne Erfahrung anzufangen ist der Normalfall, nicht die Ausnahme." },
{ schluessel: "situation", name: "Zwei Situationen",
text: "Kein Test mit richtigen Antworten. Wir wollen sehen, wie du denkst." },
{ schluessel: "schluss", name: "Zum Schluss", text: "" },
];
/** Die vier Wege, die eine Anfrage nehmen kann.
*
* Filipe: „dan soll ich annehmen ablehnen, in warteschlange stellen
* oder sofort kommunizieren können."
*
* `neu` steht nicht dabei -- das ist kein Weg, sondern der Zustand
* davor. */
export const BEWERBUNG_WEGE = [
{ schluessel: "gespraech", name: "Reden",
text: "Erst mal schreiben. Die Anfrage bleibt offen.",
/* ZUERST, und das ist Absicht: Die häufigste richtige Antwort auf
eine Bewerbung ist eine Rückfrage, keine Entscheidung. Steht
„Annehmen“ oben, klickt man es. */
weiter: "offen",
/* EINE NACHRICHT IST HIER PFLICHT, und das ist der ganze Sinn
dieses Weges. „DogFather möchte mit dir reden“ ohne einen Satz
dahinter ist keine Rückfrage, sondern eine Wartemarke -- die
Person weiß danach genau so wenig wie vorher und kann nichts
tun. Der Satz IST das Reden: Ein Zugang aus der Community steht
nicht in den Chatlisten (AUSSEN_ROLLEN), also gibt es keinen
zweiten Weg, ihr etwas zu sagen. */
nachricht: "pflicht",
nachrichtSchild: "Was willst du wissen? Der Satz steht auf ihrer Seite." },
{ schluessel: "warteschlange", name: "Warteschlange",
text: "Passt – aber gerade ist kein Platz. Bleibt liegen, nicht vergessen.",
weiter: "offen",
/* Freiwillig: „warten“ ist für sich verständlich. Ein Satz dazu
macht es freundlicher, aber ihn zu verlangen hieße, dass eine
ehrliche Antwort an der Formulierung scheitert. */
nachricht: "kann",
nachrichtSchild: "Optional: Woran es gerade liegt." },
{ schluessel: "angenommen", name: "Annehmen",
text: "Wird zum Talent und durchläuft von dort den normalen Weg.",
weiter: "talent",
nachricht: "kann",
nachrichtSchild: "Optional: ein Satz zur Begrüßung." },
{ schluessel: "abgelehnt", name: "Ablehnen",
text: "Mit einem Grund, der auch gesagt werden kann.",
weiter: "zu",
/* PFLICHT -- nicht für die Akte, für den Menschen. „Abgelehnt“ ohne
Satz ist das, was einen nicht wiederkommen lässt; und in drei
Monaten fällt der Grund niemandem mehr ein. */
nachricht: "pflicht",
nachrichtSchild: "Warum nicht? Der Satz geht an die Person." },
];
/** Wie lang eine Pflicht-Nachricht mindestens sein muss. Steht hier
* und nicht im Server: Die Oberfläche muss dieselbe Zahl kennen, sonst
* verlangt eine von beiden etwas anderes. */
export const NACHRICHT_MIN = 10;
export const BEWERBUNG_ZUSTAENDE = ["neu", ...BEWERBUNG_WEGE.map((w) => w.schluessel)];
/** Schnellzugriff. */
export const FRAGE_NACH_SCHLUESSEL = new Map(
BEWERBUNG_FRAGEN.map((f) => [f.schluessel, f]));
/** Die Pflichtfragen -- abgeleitet, damit Server und Oberfläche
* dieselbe Liste benutzen und nicht zwei pflegen. */
export const PFLICHT = BEWERBUNG_FRAGEN.filter((f) => f.pflicht).map((f) => f.schluessel);
+34
View File
@@ -0,0 +1,34 @@
/* =====================================================================
WER BEI EINER MODI-ANFRAGE WAS DARF (17.09.2026)
Zwei Mengen, mehr nicht -- und die Datei importiert absichtlich
nichts.
WARUM SIE EIGENSTÄNDIG IST: workspace-bereiche.js braucht die Frage
„darf diese Person eine Anfrage stellen?", um auf dem Brett
„Mitmachen" den Knopf zu zeigen. Würde es dafür aus
workspace-bewerbung.js importieren, entstünde ein Ring
(bewerbung -> workspace -> ... -> bereiche), und ein Ring äußert
sich in JavaScript nicht als Fehler, sondern als `undefined` an
einer Stelle, an der niemand damit rechnet.
Dasselbe Muster wie treff-tabellen.js: Ein Blatt ohne eigene
Importe kann von überall gelesen werden.
===================================================================== */
/** Wer eine Anfrage SCHICKEN darf: die Community.
*
* Das Team nicht -- wer schon dabei ist, braucht keine Anfrage. Und
* ein Modi, der sich bewirbt, wäre eine Karte im Eingang, die niemand
* einordnen kann. */
export const BEWERBUNG_ROLLEN = new Set(["gast"]);
/** Wer eine Anfrage LESEN und entscheiden darf.
*
* Filipes Entscheidung vom 17.09.2026 auf die Rückfrage: „Du und die
* rechte Hand."
*
* Die Modis sehen nichts -- eine Anfrage enthält Alter, Schwächen und
* alte Sperren, und das vor künftigen Kollegen auszubreiten wäre ein
* schlechter Anfang für beide. */
export const BEWERBUNG_LESEN = new Set(["admin", "hand"]);
+485
View File
@@ -0,0 +1,485 @@
/* =====================================================================
DIE MODI-ANFRAGE (17.09.2026)
Filipe: „ich will dass die leute sich da anonym bei mir melden
können und eine anfrage machen können für modi … und dan soll ich
annehmen ablehnen, in warteschlange stellen oder sofort
kommunizieren können. und wenn ich annehme dan soll diese person
automatisch zu talente kategorie weiter geleitet werden."
---------------------------------------------------------------------
VERTRAULICH, ABER NICHT NAMENLOS (Filipes Entscheidung)
Wer eine Anfrage schickt, ist angemeldet -- sonst wäre das Formular
ein Briefkasten für Spam, und es gäbe keinen Weg zurückzuschreiben.
Aber die Anfrage erscheint NIRGENDS öffentlich: nicht im Brett,
nicht in einer Liste, nicht in der Suche. Nur DogFather und die
rechte Hand sehen sie.
DAS IST KEINE EINSTELLUNG, SONDERN GEBAUT: Es gibt keinen Weg, der
sie an jemand anderen ausliefert. Wer einen hinzufügt, muss das
ausdrücklich tun -- ein Versehen kann es nicht sein.
---------------------------------------------------------------------
EINE ANFRAGE JE PERSON, SOLANGE SIE OFFEN IST
Nicht aus Strenge: Zwei offene Anfragen derselben Person sind zwei
Karten im Eingang, die dasselbe meinen -- und man entscheidet
zweimal über denselben Menschen, womöglich unterschiedlich. Wer
abgelehnt wurde, darf es wieder versuchen; das ist der Sinn der
Sperrfrist statt eines Verbots.
---------------------------------------------------------------------
UND DIE BRÜCKE ZU DEN TALENTEN
„Annehmen" legt einen Eintrag im Brett `talente` an und setzt ihn
auf die Stufe `angesprochen`. Nicht auf `aufgefallen`: Wer sich
selbst meldet, hat sein Interesse bereits erklärt -- die ersten
beiden Stufen fragen genau das, und sie zweimal zu stellen wäre
Beschäftigung, keine Sorgfalt.
Ab dort gilt der ganz normale Weg mit Merkmalen, Buddy, erster
Schicht und Probe. Kein zweiter Ablauf daneben.
===================================================================== */
import express from "express";
import {
db, sitzungLesen, protokolliere, echteIp, istDogFather, heuteLokal,
ROLLEN_NAME,
} from "./workspace.js";
import {
BEWERBUNG_FRAGEN, BEWERBUNG_GRUPPEN, BEWERBUNG_WEGE, BEWERBUNG_ZUSTAENDE,
FRAGE_NACH_SCHLUESSEL, PFLICHT, ANTWORT_MAX, KURZ_MAX,
NACHRICHT_MIN,
} from "./workspace-bewerbung-fragen.js";
/* DIE TALENT-TABELLEN GEHÖREN NICHT UNS — wir fordern sie nur an.
`talent_stufe` entsteht erst beim ersten Aufruf der
Entwicklungsseite. Wer hier hineinschreibt, bevor jemand sie
geöffnet hat, schreibt in eine Tabelle, die es nicht gibt — und das
passiert genau einmal: beim ersten „Annehmen" auf einem frisch
aufgesetzten Server. Also zum unangenehmsten Zeitpunkt. */
import { tabellen as entwicklungTabellen } from "./workspace-entwicklung.js";
import { BEWERBUNG_ROLLEN, BEWERBUNG_LESEN }
from "./workspace-bewerbung-rollen.js";
export const bewerbungRouter = express.Router();
const jetzt = () => new Date().toISOString();
/* DIE BEIDEN MENGEN LIEGEN IN workspace-bewerbung-rollen.js.
Sie standen hier -- bis das Brett „Mitmachen" dieselbe Frage
brauchte („darf diese Person eine Anfrage stellen?"), um den Knopf
zu zeigen. Ein Import von dort hierher wäre ein Ring gewesen;
deshalb ein Blatt ohne eigene Importe, wie bei treff-tabellen.js.
Zwei Mengen an zwei Orten wären die Bauart, die heute Mittag die
Rolle „Community" unanlegbar gemacht hat. */
const LESEN_ROLLEN = BEWERBUNG_LESEN;
const SCHICKEN_ROLLEN = BEWERBUNG_ROLLEN;
/** Nach einer Ablehnung: so viele Tage Pause.
*
* KEIN VERBOT, SONDERN EINE PAUSE. Menschen entwickeln sich, und die
* häufigste Ablehnung lautet „noch nicht" und nicht „nie". Ein
* dauerhafter Riegel wäre in einem halben Jahr falsch, und niemand
* würde ihn zurücknehmen, weil niemand mehr daran denkt. */
export const SPERRE_TAGE = 60;
/* =====================================================================
DIE TABELLEN
===================================================================== */
let bereit = false;
export function tabellen() {
if (bereit) return;
db().exec(`
CREATE TABLE IF NOT EXISTS bewerbungen (
id INTEGER PRIMARY KEY,
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
/* Wie die Person genannt werden will -- ihre eigene Angabe, nicht
der Kontoname. Sie steht hier doppelt (auch in den Antworten),
weil die Liste sie ohne Verbund zeigen muss. */
nenn_name TEXT NOT NULL,
zustand TEXT NOT NULL DEFAULT 'neu',
erstellt TEXT NOT NULL,
/* Wer entschieden hat und wann. Ohne das ist eine Ablehnung eine
Tatsache ohne Urheber -- und die erklärt hinterher niemand. */
entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
entschieden_am TEXT,
/* Der Grund. Bei einer Ablehnung der Satz, den man auch sagen
kann; sonst eine Notiz fuer das eigene Gedaechtnis. */
grund TEXT,
/* Das Talent, das daraus geworden ist. */
eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE SET NULL
)`);
db().exec(`
CREATE TABLE IF NOT EXISTS bewerbung_antwort (
bewerbung_id INTEGER NOT NULL REFERENCES bewerbungen(id) ON DELETE CASCADE,
schluessel TEXT NOT NULL,
text TEXT NOT NULL,
PRIMARY KEY (bewerbung_id, schluessel)
)`);
/* JE FRAGE EINE ZEILE und nicht ein JSON-Klumpen in einer Spalte:
Kommt eine Frage dazu, braucht es keine Schemaänderung -- und eine
entfernte Frage lässt die alten Antworten stehen, statt sie
unlesbar zu machen. */
db().exec("CREATE INDEX IF NOT EXISTS idx_bewerbung_zustand ON bewerbungen (zustand, id DESC)");
db().exec("CREATE INDEX IF NOT EXISTS idx_bewerbung_person ON bewerbungen (person_id, id DESC)");
entwicklungTabellen();
bereit = true;
}
/* =====================================================================
DIE RIEGEL
===================================================================== */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
tabellen();
next();
}
/** Nur DogFather und die rechte Hand.
*
* 404 und nicht 403: Wer „kein Zugriff" liest, weiß, dass es etwas
* gibt. Dieselbe Antwort wie überall im Haus. */
function nurLeitung(req, res, next) {
if (!LESEN_ROLLEN.has(req.person?.rolle)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
next();
}
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();
}
bewerbungRouter.use("/workspace/api/bewerbung", angemeldet);
bewerbungRouter.use("/workspace/api/bewerbungen", angemeldet);
const text = (x, max) => String(x ?? "").replace(/\r\n/g, "\n").trim().slice(0, max);
/* =====================================================================
1. DER FRAGEBOGEN — und ob man ihn gerade schicken darf
===================================================================== */
bewerbungRouter.get("/workspace/api/bewerbung", (req, res) => {
try {
const darf = SCHICKEN_ROLLEN.has(req.person.rolle);
/* Die letzte eigene Anfrage. Sie entscheidet, was die Seite zeigt:
das Formular, den Stand -- oder die Sperrfrist. */
const meine = db().prepare(
`SELECT id, zustand, erstellt, entschieden_am, grund
FROM bewerbungen WHERE person_id = ? ORDER BY id DESC LIMIT 1`)
.get(req.person.id);
let sperre = null;
if (meine?.zustand === "abgelehnt" && meine.entschieden_am) {
const tage = Math.floor((Date.now() - Date.parse(meine.entschieden_am)) / 86400000);
if (tage < SPERRE_TAGE) sperre = { tage: SPERRE_TAGE - tage, gesamt: SPERRE_TAGE };
}
const offen = meine && ["neu", "gespraech", "warteschlange"].includes(meine.zustand);
res.json({
darf,
fragen: BEWERBUNG_FRAGEN,
gruppen: BEWERBUNG_GRUPPEN,
grenzen: { lang: ANTWORT_MAX, kurz: KURZ_MAX },
meine: meine
? { zustand: meine.zustand, erstellt: meine.erstellt, grund: meine.grund ?? null }
: null,
offen: !!offen,
sperre,
/* WAS MIT DEN ANGABEN PASSIERT, steht in der Antwort und nicht
nur im Kleingedruckten der Seite: Wer ein Formular ausfüllt,
soll vorher wissen, wer es liest. */
wer_liest: "DogFather und die rechte Hand – sonst niemand.",
/* WOFUER MAN SICH MELDET -- als Wort in der Antwort.
Die Seite selbst nennt die Rolle nicht: Jede Datei unter
/workspace/ ist ohne Anmeldung aus dem offenen Netz lesbar
(gemessen 17.09.2026). Filipes Entscheidung vom 11.09., dass
die Community den Namen sieht, gilt weiter -- sie wird nur so
umgesetzt wie ueberall sonst im Haus: als Daten, nicht als
Konstante im Browser. */
rolle_name: ROLLEN_NAME.modi || null,
});
} catch (fehler) {
console.error("[bewerbung] Fragebogen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
2. ABSCHICKEN
===================================================================== */
bewerbungRouter.post("/workspace/api/bewerbung", express.json({ limit: "64kb" }),
gleicheHerkunft, (req, res) => {
try {
if (!SCHICKEN_ROLLEN.has(req.person.rolle)) {
return res.status(403).json({ fehler: "Anfragen kommen aus der Community." });
}
/* SCHON EINE OFFEN? Dann keine zweite. */
const offen = db().prepare(
`SELECT id FROM bewerbungen
WHERE person_id = ? AND zustand IN ('neu','gespraech','warteschlange')`)
.get(req.person.id);
if (offen) {
return res.status(409).json({
fehler: "Deine Anfrage liegt schon vor – sie ist noch offen.",
});
}
/* UND DIE SPERRFRIST NACH EINER ABLEHNUNG. */
const letzte = db().prepare(
`SELECT zustand, entschieden_am FROM bewerbungen
WHERE person_id = ? ORDER BY id DESC LIMIT 1`).get(req.person.id);
if (letzte?.zustand === "abgelehnt" && letzte.entschieden_am) {
const tage = Math.floor((Date.now() - Date.parse(letzte.entschieden_am)) / 86400000);
if (tage < SPERRE_TAGE) {
return res.status(429).json({
fehler: `Das geht in ${SPERRE_TAGE - tage} Tagen wieder. `
+ "Eine Absage heißt hier „noch nicht“, nicht „nie“.",
});
}
}
/* DIE ANTWORTEN. Nur bekannte Schlüssel -- ein erfundener wäre
eine Zeile in der Datenbank, die keine Frage beantwortet. */
const roh = req.body?.antworten;
if (!roh || typeof roh !== "object") {
return res.status(400).json({ fehler: "Da fehlen die Antworten." });
}
const antworten = new Map();
for (const [k, v] of Object.entries(roh)) {
const frage = FRAGE_NACH_SCHLUESSEL.get(k);
if (!frage) continue;
const wert = text(v, frage.art === "kurz" ? KURZ_MAX : ANTWORT_MAX);
if (wert) antworten.set(k, wert);
}
/* PFLICHTFRAGEN -- die Liste kommt aus dem Katalog, nicht von
hier. Zwei Listen liefen beim ersten Hinzufügen auseinander,
und die Oberfläche würde dann etwas verlangen, das der Server
nicht prüft (oder umgekehrt). */
const fehlt = PFLICHT.filter((k) => !antworten.has(k));
if (fehlt.length) {
return res.status(400).json({
fehler: "Ein paar Antworten fehlen noch.",
fehlt,
});
}
const nenn = antworten.get("name");
const { lastInsertRowid } = db().prepare(
`INSERT INTO bewerbungen (person_id, nenn_name, zustand, erstellt)
VALUES (?,?, 'neu', ?)`).run(req.person.id, nenn, jetzt());
const id = Number(lastInsertRowid);
const rein = db().prepare(
"INSERT INTO bewerbung_antwort (bewerbung_id, schluessel, text) VALUES (?,?,?)");
for (const [k, v] of antworten) rein.run(id, k, v);
/* IM PROTOKOLL STEHT KEIN INHALT. Die Anfrage ist vertraulich --
das gilt auch für das Protokoll, das mehr Leute lesen als die
Anfrage selbst. */
protokolliere("bewerbung_eingegangen", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${id}`,
});
res.status(201).json({ id, zustand: "neu" });
} catch (fehler) {
console.error("[bewerbung] Abschicken:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
2b. NUR DIE ZAHL — für den Weg von der Talente-Seite hierher
Eine eigene, winzige Auskunft und NICHT der volle Eingang: Die
Talente-Seite braucht „liegt da etwas?", nicht fünfzehn Antworten je
Anfrage. Diese Sätze in eine Seite zu laden, die sie nicht anzeigt,
wäre vertraulicher Text an einem Ort, an dem ihn niemand erwartet --
und im Zwischenspeicher des Browsers landet er trotzdem.
===================================================================== */
bewerbungRouter.use("/workspace/api/bewerbungen-zahl", angemeldet);
bewerbungRouter.get("/workspace/api/bewerbungen-zahl", nurLeitung, (req, res) => {
try {
const z = db().prepare(
`SELECT
SUM(CASE WHEN zustand = 'neu' THEN 1 ELSE 0 END) AS neu,
SUM(CASE WHEN zustand IN ('neu','gespraech','warteschlange')
THEN 1 ELSE 0 END) AS offen
FROM bewerbungen`).get() || {};
res.json({ neu: z.neu || 0, offen: z.offen || 0 });
} catch (fehler) {
console.error("[bewerbung] Zahl:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
3. DER EINGANG — nur DogFather und die rechte Hand
===================================================================== */
bewerbungRouter.get("/workspace/api/bewerbungen", nurLeitung, (req, res) => {
try {
const zeilen = db().prepare(`
SELECT b.id, b.person_id, b.nenn_name, b.zustand, b.erstellt, b.grund,
b.entschieden_am, b.eintrag_id,
p.name AS konto_name, p.rolle AS konto_rolle,
e.name AS von_name
FROM bewerbungen b
LEFT JOIN personen p ON p.id = b.person_id
LEFT JOIN personen e ON e.id = b.entschieden_von
ORDER BY
/* NEUE ZUERST, DANN WAS LIEGT. Die Reihenfolge ist die
Arbeitsanweisung dieser Seite -- wie bei den Talenten. */
CASE b.zustand WHEN 'neu' THEN 0 WHEN 'gespraech' THEN 1
WHEN 'warteschlange' THEN 2 ELSE 3 END,
b.id DESC`).all();
const ids = zeilen.map((z) => z.id);
const je = new Map();
if (ids.length) {
const platz = ids.map(() => "?").join(", ");
for (const a of db().prepare(
`SELECT bewerbung_id, schluessel, text FROM bewerbung_antwort
WHERE bewerbung_id IN (${platz})`).all(...ids)) {
if (!je.has(a.bewerbung_id)) je.set(a.bewerbung_id, {});
je.get(a.bewerbung_id)[a.schluessel] = a.text;
}
}
const heute = heuteLokal();
res.json({
fragen: BEWERBUNG_FRAGEN,
gruppen: BEWERBUNG_GRUPPEN,
wege: BEWERBUNG_WEGE,
nachricht_min: NACHRICHT_MIN,
/* Ob DIESE Person auch entscheiden darf -- die rechte Hand sieht
mit und entscheidet mit (Filipes Entscheidung vom 17.09.). */
darf_entscheiden: true,
ich: { rolle: req.person.rolle, dogfather: istDogFather(req.person) },
bewerbungen: zeilen.map((z) => ({
...z,
antworten: je.get(z.id) || {},
/* WIE LANGE LIEGT SIE SCHON? Eine Anfrage, die drei Wochen
unbeantwortet liegt, ist eine Absage, die niemand
ausgesprochen hat. */
tage: Math.max(0, Math.floor(
(Date.parse(heute + "T12:00:00Z") - Date.parse(z.erstellt)) / 86400000)),
})),
zahlen: Object.fromEntries(BEWERBUNG_ZUSTAENDE.map((z) =>
[z, zeilen.filter((x) => x.zustand === z).length])),
});
} catch (fehler) {
console.error("[bewerbung] Eingang:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
4. ENTSCHEIDEN — und bei „Annehmen" die Brücke zu den Talenten
===================================================================== */
bewerbungRouter.put("/workspace/api/bewerbung/:id/weg", express.json({ limit: "8kb" }),
gleicheHerkunft, nurLeitung, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const b = db().prepare(
"SELECT id, person_id, nenn_name, zustand, eintrag_id FROM bewerbungen WHERE id = ?")
.get(id);
if (!b) return res.status(404).json({ fehler: "nicht_gefunden" });
const wegSchluessel = String(req.body?.weg ?? "");
const weg = BEWERBUNG_WEGE.find((w) => w.schluessel === wegSchluessel);
if (!weg) return res.status(400).json({ fehler: "Unbekannte Entscheidung." });
const grund = text(req.body?.grund, 600);
/* OB EINE NACHRICHT NÖTIG IST, STEHT IM KATALOG -- nicht hier.
Vorher stand an dieser Stelle `weg.schluessel === "abgelehnt"`
und die Zahl 10. Dieselbe Regel hätte die Oberfläche ein
zweites Mal gebraucht, und zwei Listen für dieselbe Frage sind
genau die Bauart, die heute Mittag die Rolle „Community"
unanlegbar gemacht hat: Die Prüfung las eine Liste, die für
etwas anderes gedacht war.
Jetzt liest die Oberfläche `nachricht` und `NACHRICHT_MIN` aus
derselben Antwort, die der Server selbst prüft. */
if (weg.nachricht === "pflicht" && grund.length < NACHRICHT_MIN) {
return res.status(400).json({
fehler: weg.schluessel === "abgelehnt"
? "Schreib kurz dazu, warum – der Satz geht an die Person."
: "Schreib kurz dazu, worum es geht – sonst weiß sie nichts damit anzufangen.",
});
}
let eintragId = b.eintrag_id ?? null;
/* ---- DIE BRÜCKE ---------------------------------------------- */
if (weg.weiter === "talent" && !eintragId) {
const t = jetzt();
const antworten = new Map(db().prepare(
"SELECT schluessel, text FROM bewerbung_antwort WHERE bewerbung_id = ?")
.all(id).map((z) => [z.schluessel, z.text]));
/* Der Eintrag heißt wie die Person genannt werden will. Der
Kontoname steht bewusst nicht im Titel: Auf dem Brett lesen
ihn später auch Leute, die die Anfrage nie gesehen haben. */
const einsatz = antworten.get("womit") || null;
const { lastInsertRowid } = db().prepare(`
INSERT INTO eintraege
(bereich, art, titel, text, datum, status, erstellt, erstellt_von, einsatz)
VALUES ('talente', 'empfohlen', ?, ?, ?, 'offen', ?, ?, ?)`)
.run(b.nenn_name,
`Hat sich selbst gemeldet (Anfrage #${id}).`
+ (antworten.get("warum") ? `\n\nWarum: ${antworten.get("warum")}` : ""),
t.slice(0, 10), t, req.person.id, einsatz);
eintragId = Number(lastInsertRowid);
/* AUF „ANGESPROCHEN" und nicht auf „aufgefallen": Die ersten
beiden Stufen fragen, ob überhaupt Interesse besteht. Wer
sich selbst gemeldet hat, hat das beantwortet -- die Frage
ein zweites Mal zu stellen wäre Beschäftigung, keine
Sorgfalt. */
db().prepare(`
INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id, notiz)
VALUES (?, 'angesprochen', ?, ?, ?)
ON CONFLICT(eintrag_id) DO UPDATE SET stufe = 'angesprochen', seit = excluded.seit`)
.run(eintragId, t, req.person.id, `Aus der Anfrage #${id}.`);
}
db().prepare(`
UPDATE bewerbungen
SET zustand = ?, grund = ?, entschieden_von = ?, entschieden_am = ?,
eintrag_id = ?
WHERE id = ?`)
.run(weg.schluessel, grund || null, req.person.id, jetzt(), eintragId, id);
protokolliere("bewerbung_entschieden", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${id} -> ${weg.schluessel}`,
});
res.json({
ok: true, zustand: weg.schluessel, eintrag_id: eintragId,
/* Wohin man jetzt springen kann. */
weiter: eintragId ? `/workspace/talente.html` : null,
});
} catch (fehler) {
console.error("[bewerbung] Entscheiden:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+18
View File
@@ -318,6 +318,24 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
WHERE p.rolle = 'creator' AND p.aktiv = 1 AND f.person_id IS NULL`, []));
}
/* ---------- Anfragen für Modi ----------
WARUM ES DAFUER EINEN HINWEIS BRAUCHT: Die Anfragen haben
ausdruecklich KEINE Kachel -- sie stehen auf der Talente-Seite,
weil sie der Anfang desselben Trichters sind. Damit gibt es auf
der Startseite nichts, was auf sie zeigt.
Und eine Anfrage, die drei Wochen unbeantwortet liegt, ist eine
Absage, die niemand ausgesprochen hat. Genau dafuer ist die
Glocke da.
`dazu` prueft selbst, ob die Person diese Seite hier oeffnen
darf -- ein Hinweis auf eine gesperrte Seite waere derselbe
Fehler wie am 15.09. beim Start-Check. */
dazu("bewerbungen_neu", "offen",
"neue Anfrage für Modi", "bewerbungen.html",
zaehle("SELECT COUNT(*) n FROM bewerbungen WHERE zustand = 'neu'", []));
hinweise.sort((x, y) => (RANG[x.stufe] - RANG[y.stufe]) || (y.anzahl - x.anzahl));
res.json({ hinweise, stand: new Date().toISOString() });
} catch (fehler) {
+23
View File
@@ -4318,6 +4318,29 @@ const OHNE_KACHEL_UEBERALL = new Set([
aber ueber „Deine Karte“ und ueber den Verweis von befinden.html
dorthin -- und beides ist seine eigene Sache. */
"/workspace/entwicklung.html",
/* DIE MODI-ANFRAGE UND IHR EINGANG (17.09.2026).
BEIDE OHNE KACHEL, und zwar mit Grund -- nicht, weil kein Platz
war:
bewerben.html gehoert INS Brett "Mitmachen" und nicht daneben.
Eine achte Kachel im Treff waere ein zweiter Ort fuer dieselbe
Sache; das Brett erklaert, wie man dazugehoert, und der Knopf
steht genau dort, wo die Frage entsteht.
bewerbungen.html gehoert AUF die Talente-Seite. Die Anfragen
sind der Anfang desselben Trichters -- wer angenommen wird,
steht eine Sekunde spaeter dort als Talent. Eine eigene Kachel
daneben haette den Weg in zwei Seiten zerlegt, die man
abwechselnd aufmacht.
(Nebenbei: Ein 38. Kachelton laesst sich gemessen nicht mehr
unterbringen. Alle 37 sind vergeben, und der beste freie Rest im
Hausrahmen liegt bei einem Abstand von 39 -- das ist Dunkelrot,
also die Farbe von Spicy Media und die Farbe fuer Alarm. Das war
ein Hinweis, kein Grund.) */
"/workspace/bewerben.html",
"/workspace/bewerbungen.html",
]);
/** Gibt es diese Seite auf der Adresse, auf der die Person gerade steht? */
+4 -4
View File
@@ -22,25 +22,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609171416",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609171558",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609171416",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609171558",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171416",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171558",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171416",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171558",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+274
View File
@@ -0,0 +1,274 @@
/* =====================================================================
DIE ANFRAGE AUS DER COMMUNITY (17.09.2026)
Zwei Seiten teilen sich diese Datei: der Fragebogen (bewerben.html)
und der Eingang (bewerbungen.html). Eine gemeinsame Datei und nicht
zwei -- die Anfrage soll in beiden Richtungen gleich aussehen, und
zwei Dateien wären zwei Gelegenheiten, eine davon zu vergessen.
DIE FLAECHEN SIND DECKEND, UND DAS WAR EIN FEHLER WERT.
Zuerst stand hier `color-mix(in srgb, var(--tinte) 94%, transparent)`.
Das mischt mit DURCHSICHTIG -- 94 % heisst also: 6 % Foto scheint
durch. Auf dem Bildschirmfoto stand der Schriftzug der Buehne mitten
im Text einer Anfrage. Gefunden hat das kein Messwert, sondern das
Hinsehen; dieselbe Stelle hat mich am 17.09. schon einmal erwischt
(Talente, leerer Zustand).
Richtig ist `#ffffff 6%` -- derselbe Ton, aber deckend. So macht es
das Haus in .t-karte.
ALLES GEDECKT, NICHTS BLINKT. Hier liest jemand Sätze, die ein
anderer über sich selbst geschrieben hat; und auf der anderen Seite
sitzt jemand, der gerade nach Worten sucht. Beides verträgt keine
Signalfarben. Rot kommt nur an einer Stelle vor: an einem Feld, das
wirklich fehlt -- und auch dort nur als Rand.
===================================================================== */
/* =====================================================================
1. DER FRAGEBOGEN
===================================================================== */
.bw-stand {
margin: 0 0 22px;
padding: 16px 18px;
border: 1px solid var(--rand);
border-radius: 14px;
/* EIGENE FLÄCHE, KEIN DURCHSCHEINEN. Auf der Bühne liegt ein Motiv;
ohne deckenden Grund steht der Text darauf statt darüber. Das ist
am 17.09. schon einmal passiert (Talente, leerer Zustand). */
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.bw-stand__wort { margin: 0; font-size: 1.05rem; font-weight: 700; }
.bw-stand__grund {
margin: 10px 0 0;
padding: 10px 12px;
border-inline-start: 3px solid var(--akzent);
border-radius: 0 8px 8px 0;
background: color-mix(in srgb, var(--akzent) 8%, transparent);
line-height: 1.55;
}
.bw-stand__sperre { margin: 0; font-size: 1.05rem; font-weight: 700; }
.bw-stand .knopf { margin-top: 14px; }
.bw-bogen { display: block; }
.bw-gruppe {
margin: 0 0 26px;
padding: 16px 18px;
border: 1px solid var(--rand);
border-radius: 14px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.bw-gruppe__kopf { margin-bottom: 14px; }
.bw-gruppe__titel { margin: 0; font-size: 1.02rem; font-weight: 700; }
.bw-gruppe__unter { margin: 4px 0 0; font-size: .86rem; opacity: .75; }
.bw-feld { margin: 0 0 18px; }
.bw-feld:last-child { margin-bottom: 0; }
.bw-feld__schild {
display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
margin: 0 0 4px;
font-weight: 600;
}
/* „nötig" und „freiwillig" stehen als WORT da und nicht als Sternchen.
Ein Sternchen muss man erklären; wer die Erklärung überliest, füllt
aus Vorsicht alles aus und schreibt am Ende nirgends etwas
Ehrliches. */
.bw-feld__pflicht,
.bw-feld__frei {
font-size: .74rem; font-weight: 600;
letter-spacing: .04em; text-transform: uppercase;
padding: 2px 7px; border-radius: 999px;
}
.bw-feld__pflicht {
color: var(--akzent);
background: color-mix(in srgb, var(--akzent) 12%, transparent);
}
.bw-feld__frei { opacity: .6; background: color-mix(in srgb, var(--rand) 40%, transparent); }
.bw-feld__hinweis { margin: 0 0 6px; font-size: .86rem; opacity: .75; line-height: 1.5; }
.bw-feld__eingabe {
display: block; width: 100%;
padding: 10px 12px;
font: inherit; font-size: .95rem; line-height: 1.55;
color: inherit;
background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%);
border: 1px solid var(--rand); border-radius: 10px;
resize: vertical;
}
.bw-feld__eingabe:focus-visible {
outline: 2px solid var(--akzent); outline-offset: 2px;
border-color: var(--akzent);
}
/* Ein fehlendes Pflichtfeld wird MARKIERT, nicht angeschrien: Rand und
ein Wort in der Meldung oben. Rot flächig wäre auf fünfzehn Feldern
eine Wand. */
.bw-feld--fehlt .bw-feld__eingabe {
border-color: color-mix(in srgb, var(--warn) 70%, var(--rand));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 40%, transparent);
}
.bw-feld__zaehler { margin: 4px 0 0; font-size: .78rem; opacity: .65; text-align: end; }
.bw-wer-liest {
margin: 0 0 14px;
font-size: .86rem; opacity: .8;
padding: 10px 12px;
border: 1px dashed var(--rand); border-radius: 10px;
}
.bw-abschluss { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
/* =====================================================================
2. DER EINGANG
===================================================================== */
.bw-zahlen {
display: flex; gap: 12px; flex-wrap: wrap;
margin: 0 0 22px;
}
.bw-karte {
margin: 0 0 20px;
padding: 16px 18px;
border: 1px solid var(--rand);
border-radius: 14px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
/* Der Zustand als feine Kante links -- er sagt beim Scrollen, was
erledigt ist, ohne dass man das Schild lesen muss. */
.bw-karte[data-zustand="neu"] { border-inline-start: 3px solid var(--akzent); }
.bw-karte[data-zustand="gespraech"],
.bw-karte[data-zustand="warteschlange"] {
border-inline-start: 3px solid color-mix(in srgb, var(--akzent) 45%, var(--rand));
}
/* ENTSCHIEDENE KARTEN TRETEN ZURUECK -- ueber die SCHRIFT, nicht ueber
`opacity`. Ein durchsichtiges Element nimmt seine Flaeche mit, und
hinter dieser Seite liegt ein Foto: Aus "etwas leiser" waere wieder
"das Motiv scheint durch" geworden. Derselbe Fehler, zehn Zeilen
weiter oben. */
.bw-karte[data-zustand="angenommen"] .bw-karte__bogen,
.bw-karte[data-zustand="abgelehnt"] .bw-karte__bogen,
.bw-karte[data-zustand="angenommen"] .bw-karte__kopf,
.bw-karte[data-zustand="abgelehnt"] .bw-karte__kopf { color: color-mix(in srgb, var(--text) 78%, transparent); }
.bw-karte__kopf {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px; flex-wrap: wrap;
margin-bottom: 12px;
}
.bw-karte__wer { min-width: 0; }
.bw-karte__name { margin: 0; font-size: 1.08rem; font-weight: 700; }
.bw-karte__unter { margin: 3px 0 0; font-size: .84rem; opacity: .72; }
.bw-marke {
font-size: .76rem; font-weight: 600;
letter-spacing: .04em; text-transform: uppercase;
padding: 3px 9px; border-radius: 999px;
border: 1px solid var(--rand);
background: color-mix(in srgb, var(--tinte) 90%, #ffffff 10%);
}
.bw-karte__frueher { margin: 0 0 12px; }
.bw-karte__grund {
margin: 0;
padding: 10px 12px;
border-inline-start: 3px solid var(--akzent);
border-radius: 0 8px 8px 0;
background: color-mix(in srgb, var(--akzent) 8%, transparent);
line-height: 1.55;
}
.bw-karte__bogen { margin: 0 0 16px; }
.bw-karte__gruppe {
margin: 16px 0 8px;
font-size: .78rem; font-weight: 700;
letter-spacing: .06em; text-transform: uppercase;
opacity: .65;
}
.bw-karte__gruppe:first-child { margin-top: 0; }
.bw-antwort { margin: 0 0 10px; }
.bw-antwort__frage { margin: 0; font-size: .84rem; opacity: .7; }
.bw-antwort__text {
margin: 2px 0 0;
line-height: 1.6;
white-space: pre-wrap;
}
/* Was NICHT beantwortet wurde, steht dabei. Eine übersprungene
freiwillige Frage ist selbst eine Auskunft. */
.bw-karte__ohne { margin: 14px 0 0; font-size: .8rem; opacity: .55; line-height: 1.5; }
.bw-wahl {
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--rand);
}
.bw-wahl__schild { margin: 0 0 8px; font-size: .84rem; opacity: .75; }
.bw-wahl__knoepfe { display: flex; gap: 8px; flex-wrap: wrap; }
.bw-weg {
font: inherit; font-size: .88rem; font-weight: 600;
color: inherit;
padding: 8px 14px;
min-height: 40px;
border: 1px solid var(--rand); border-radius: 999px;
background: color-mix(in srgb, var(--tinte) 90%, #ffffff 10%);
cursor: pointer;
}
.bw-weg:hover { border-color: color-mix(in srgb, var(--akzent) 60%, var(--rand)); }
.bw-weg[aria-pressed="true"] {
border-color: var(--akzent);
background: color-mix(in srgb, var(--akzent) 16%, transparent);
}
.bw-weg:disabled { opacity: .45; cursor: default; }
.bw-weg:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.bw-wahl__feld { margin: 12px 0 0; }
.bw-wahl__feldschild { display: block; margin: 0 0 4px; font-size: .86rem; opacity: .8; }
.bw-wahl__eingabe {
display: block; width: 100%;
padding: 10px 12px;
font: inherit; font-size: .93rem; line-height: 1.55;
color: inherit;
background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%);
border: 1px solid var(--rand); border-radius: 10px;
resize: vertical;
}
.bw-wahl__eingabe:focus-visible {
outline: 2px solid var(--akzent); outline-offset: 2px;
border-color: var(--akzent);
}
.bw-wahl .knopf { margin-top: 12px; }
.bw-leer {
padding: 22px 20px;
border: 1px dashed var(--rand); border-radius: 14px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.bw-leer__wort { margin: 0 0 6px; font-size: 1.02rem; font-weight: 700; }
.bw-hinweis {
margin: 0 0 16px;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, var(--akzent) 45%, var(--rand));
border-radius: 12px;
background: color-mix(in srgb, var(--akzent) 10%, transparent);
}
/* Der Weg von der Talente-Seite hierher. */
.bw-tor {
display: flex; align-items: center; justify-content: space-between;
gap: 14px; flex-wrap: wrap;
margin: 0 0 20px;
padding: 12px 16px;
border: 1px solid var(--rand); border-radius: 12px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.bw-tor[data-neu="ja"] {
border-color: color-mix(in srgb, var(--akzent) 50%, var(--rand));
background: color-mix(in srgb, var(--akzent) 10%, transparent);
}
.bw-tor__wort { margin: 0; font-size: .95rem; }
+7 -1
View File
@@ -1497,7 +1497,13 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe
KEINE SCHWELLE, KEINE WARNFARBE. Was „zu viel" ist, hängt vom
Menschen ab; eine feste Grenze wäre geraten. Was NICHT geraten
ist: dass etwas überfällig liegt. Nur das wird markiert. */
const zielbar = personenListe.filter((p) => p.rolle === 'modi' || p.rolle === 'hand');
/* WER IN FRAGE KOMMT, SAGT DER SERVER (17.09.2026).
Hier standen zwei Rollennamen. Diese Datei ist ohne Anmeldung
aus dem offenen Netz lesbar -- damit war der verborgene Zugang
darin nachzulesen. Gefunden von der Wortleck-Pruefung, nicht vom
Auge. */
const zielbar = personenListe.filter((p) => p.fuer_katalog);
if (zielbar.length && $('feld-verant') && !$('feld-verant').hidden) {
/* DIE DRITTE REIHE IST ETWAS ANDERES als die zwei darueber: Sie
waehlt keinen Ausschnitt des Katalogs, sondern einen MENSCHEN.
+27
View File
@@ -1101,6 +1101,32 @@
ziel.append(liste);
}
/* =====================================================================
DER NAECHSTE SCHRITT (17.09.2026)
Der Server schickt entweder einen fertigen Block oder nichts. Diese
Funktion weiss nicht, worum es geht -- und genau das ist der Punkt:
Die Frage "darf diese Person?" haengt an der Rolle, und diese Datei
kann ohne Anmeldung aus dem offenen Netz gelesen werden.
Beim naechsten Bereich, der so einen Schritt braucht, ist hier
nichts zu tun.
===================================================================== */
function weiterZeigen(w) {
const kasten = $('weiter');
if (!kasten) return;
kasten.textContent = '';
if (!w || !w.ziel) { kasten.hidden = true; return; }
kasten.hidden = false;
kasten.dataset.neu = 'ja';
const text = el('div');
text.append(el('p', 'bw-tor__wort', w.titel || ''));
if (w.text) text.append(el('p', 'leise', w.text));
const hin = el('a', 'knopf', w.knopf || 'Weiter');
hin.href = w.ziel;
kasten.append(text, hin);
}
async function laden() {
try {
await uebernahmenLaden();
@@ -1109,6 +1135,7 @@
const daten = await a.json();
einstellung = daten.einstellung;
treffBrett = !!daten.treff;
weiterZeigen(daten.weiter);
await lageHolen();
filterBauen(daten.eintraege || []);
+279
View File
@@ -0,0 +1,279 @@
/* =====================================================================
DIE ANFRAGE AUS DER COMMUNITY -- der Fragebogen (17.09.2026)
Filipe: „ich will dass die leute sich da anonym bei mir melden können
und eine anfrage machen können ... wie ein fragebogen wieso die
person es werden will warum sie sollte, positiv negative sachen."
(Sein Satz nennt die Rolle beim Namen -- hier steht sie nicht: Diese
Datei ist ohne Anmeldung aus dem offenen Netz lesbar.)
DIE FRAGEN STEHEN NICHT HIER. Sie kommen vom Server, mit ihren
Gruppen, ihren Pflichtmarken und den Längengrenzen. Eine zweite Liste
im Browser wäre die, die beim nächsten Nachschärfen auseinanderläuft
-- und dann verlangt das Formular etwas, das der Server nicht prüft,
oder umgekehrt. Genau dieser Fehler hat heute Mittag die Rolle
„Community" zwei Stunden lang unanlegbar gemacht.
DER ENTWURF BLEIBT IM BROWSER, NICHT AUF DEM SERVER.
Fünfzehn Fragen sind nichts, was man zwischen zwei Streams tippt. Wer
aufhören und später weitermachen will, muss das können -- sonst wird
die Anfrage nie fertig, und das ist dieselbe Wirkung wie eine Absage.
Der Entwurf liegt deshalb auf dem Gerät.
Und er liegt AUSDRÜCKLICH NICHT auf dem Server: Ein halb ausgefülltes
Formular dort wäre bereits eine Anfrage, die niemand gestellt hat --
lesbar für DogFather, bevor die Person entschieden hat, ob sie sie
überhaupt schicken will. Beim Abschicken wird der Entwurf gelöscht.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
const ENTWURF = 'dogi-bewerbung-entwurf';
const melde = (t) => {
const f = $('fehler');
if (f) { f.textContent = t || ''; f.hidden = !t; }
};
const el = (art, klasse, text) => {
const k = document.createElement(art);
if (klasse) k.className = klasse;
if (text != null) k.textContent = text;
return k;
};
/* Der Entwurf. Ein kaputter Eintrag darf die Seite nicht aufhalten --
dann lieber ohne Entwurf anfangen als gar nicht. */
const entwurfLesen = () => {
try { return JSON.parse(localStorage.getItem(ENTWURF) || '{}') || {}; }
catch { return {}; }
};
const entwurfSchreiben = (w) => {
try { localStorage.setItem(ENTWURF, JSON.stringify(w)); } catch { /* voll oder gesperrt */ }
};
const ZUSTAND_WORT = {
neu: 'Deine Anfrage liegt vor.',
gespraech: 'DogFather möchte mit dir reden.',
warteschlange: 'Du stehst auf der Warteliste.',
angenommen: 'Du bist dabei.',
abgelehnt: 'Diesmal nicht.',
};
(async () => {
let r;
try {
const a = await fetch('/workspace/api/bewerbung', { credentials: 'same-origin' });
if (a.status === 401) { location.assign('/workspace/'); return; }
if (!a.ok) { melde('Der Fragebogen ist gerade nicht erreichbar.'); return; }
r = await a.json();
} catch {
melde('Der Fragebogen ist gerade nicht erreichbar.');
return;
}
$('ladezeile').hidden = true;
/* DAS WORT KOMMT VOM SERVER. Bleibt es aus, steht „Anfrage
stellen" da -- eine ehrliche Ueberschrift, die nichts Falsches
behauptet. Ein Platzhalter („…") waere hier schlechter: Er sieht
kaputt aus, obwohl die Seite vollstaendig arbeitet. */
if (r.rolle_name) {
const t = $('titel');
if (t) t.textContent = r.rolle_name + ' werden';
const m = $('marke-was');
if (m) m.textContent = r.rolle_name + ' werden';
document.title = r.rolle_name + ' werden · Der Treff';
}
/* ---------------------------------------------------------------
1. WAS SCHON IST -- Stand oder Sperrfrist
--------------------------------------------------------------- */
const stand = $('stand');
let zeigeFormular = r.darf;
if (r.meine) {
stand.hidden = false;
stand.append(el('p', 'bw-stand__wort',
ZUSTAND_WORT[r.meine.zustand] || 'Deine Anfrage liegt vor.'));
if (r.meine.grund) {
/* DER SATZ AUS DER ENTSCHEIDUNG GEHÖRT DER PERSON. Er wird beim
Ablehnen verlangt, und zwar genau dafür -- nicht für die
Akte. Ihn hier wegzulassen hieße, ihn umsonst verlangt zu
haben. */
stand.append(el('p', 'bw-stand__grund', r.meine.grund));
}
if (r.offen) {
stand.append(el('p', 'leise',
'Solange sie offen ist, kannst du keine zweite schicken. '
+ 'Du hörst von uns – das kann ein paar Tage dauern.'));
zeigeFormular = false;
}
}
if (r.sperre) {
stand.hidden = false;
stand.append(el('p', 'bw-stand__sperre',
'In ' + r.sperre.tage + ' Tagen kannst du es wieder versuchen.'));
stand.append(el('p', 'leise',
'Eine Absage heißt hier „noch nicht“, nicht „nie“. '
+ 'Bis dahin: mitreden, Wünsche einstellen, dabei sein.'));
zeigeFormular = false;
}
if (!r.darf) {
stand.hidden = false;
stand.append(el('p', 'leise',
'Diese Seite ist für die Community. Du bist schon im Team.'));
}
if (!zeigeFormular) return;
/* ---------------------------------------------------------------
2. DER BOGEN
--------------------------------------------------------------- */
const bogen = $('bogen');
const ziel = $('gruppen');
const werte = entwurfLesen();
const felder = new Map();
for (const g of r.gruppen) {
const fragen = r.fragen.filter((f) => f.gruppe === g.schluessel);
if (!fragen.length) continue;
const block = el('section', 'bw-gruppe');
const kopf = el('div', 'bw-gruppe__kopf');
kopf.append(el('h2', 'bw-gruppe__titel', g.name));
if (g.unter) kopf.append(el('p', 'bw-gruppe__unter', g.unter));
block.append(kopf);
for (const f of fragen) {
const kurz = f.art === 'kurz';
const grenze = kurz ? r.grenzen.kurz : r.grenzen.lang;
const id = 'f-' + f.schluessel;
const feld = el('div', 'bw-feld');
const schild = el('label', 'bw-feld__schild');
schild.htmlFor = id;
schild.append(document.createTextNode(f.titel));
/* PFLICHT WIRD ANGESCHRIEBEN, NICHT ERZWUNGEN-BIS-ES-KLEMMT.
Ein Sternchen ohne Erklärung ist eine Falle; hier steht das
Wort. Und was NICHT Pflicht ist, steht auch dran -- sonst
füllt man aus Vorsicht alles aus und schreibt bei drei
Fragen nichts Ehrliches mehr. */
schild.append(el('span', f.pflicht ? 'bw-feld__pflicht' : 'bw-feld__frei',
f.pflicht ? 'nötig' : 'freiwillig'));
feld.append(schild);
if (f.hinweis) feld.append(el('p', 'bw-feld__hinweis', f.hinweis));
const eingabe = kurz ? document.createElement('input')
: document.createElement('textarea');
eingabe.id = id;
eingabe.className = 'bw-feld__eingabe';
eingabe.maxLength = grenze;
if (kurz) { eingabe.type = 'text'; eingabe.autocomplete = 'off'; }
else { eingabe.rows = 4; }
eingabe.value = werte[f.schluessel] || '';
feld.append(eingabe);
/* Der Zähler zählt, was noch geht -- nicht, was schon steht.
„Noch 80 Zeichen" ist eine Auskunft; „180 von 1200" ist eine
Aufforderung, mehr zu schreiben. Deshalb meldet er sich auch
erst, wenn es eng wird. */
const zaehler = el('p', 'bw-feld__zaehler');
const nachziehen = () => {
const rest = grenze - eingabe.value.length;
zaehler.textContent = rest < 80 ? ('noch ' + rest + ' Zeichen') : '';
zaehler.hidden = rest >= 80;
};
nachziehen();
eingabe.addEventListener('input', () => {
werte[f.schluessel] = eingabe.value;
entwurfSchreiben(werte);
nachziehen();
feld.classList.remove('bw-feld--fehlt');
});
feld.append(zaehler);
block.append(feld);
felder.set(f.schluessel, { feld, eingabe, frage: f });
}
ziel.append(block);
}
$('wer-liest').textContent = 'Wer das liest: ' + (r.wer_liest || '');
bogen.hidden = false;
/* ---------------------------------------------------------------
3. ABSCHICKEN
--------------------------------------------------------------- */
bogen.addEventListener('submit', async (e) => {
e.preventDefault();
melde('');
/* Die Pflichtliste kommt aus den Fragen des Servers -- dieselbe
Quelle, die er selbst prüft. */
const fehlt = [...felder.values()]
.filter((x) => x.frage.pflicht && !x.eingabe.value.trim());
for (const x of felder.values()) x.feld.classList.remove('bw-feld--fehlt');
if (fehlt.length) {
for (const x of fehlt) x.feld.classList.add('bw-feld--fehlt');
melde(fehlt.length === 1
? 'Eine Antwort fehlt noch – sie ist markiert.'
: fehlt.length + ' Antworten fehlen noch – sie sind markiert.');
fehlt[0].eingabe.focus();
fehlt[0].feld.scrollIntoView({ block: 'center', behavior: 'smooth' });
return;
}
const knopf = $('abschicken');
knopf.disabled = true;
knopf.textContent = 'wird geschickt …';
const antworten = {};
for (const [k, x] of felder) {
const v = x.eingabe.value.trim();
if (v) antworten[k] = v;
}
try {
const a = await fetch('/workspace/api/bewerbung', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ antworten }),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) {
melde(d.fehler || 'Das hat nicht geklappt.');
knopf.disabled = false;
knopf.textContent = 'Anfrage abschicken';
return;
}
/* ERST JETZT den Entwurf löschen -- nicht vorher. Wäre er beim
Absenden schon weg und der Server antwortet nicht, stünde man
vor einem leeren Formular und einer halben Stunde Arbeit im
Nichts. */
try { localStorage.removeItem(ENTWURF); } catch { /* egal */ }
bogen.hidden = true;
const fertig = $('stand');
fertig.hidden = false;
fertig.textContent = '';
fertig.append(el('p', 'bw-stand__wort', 'Angekommen.'));
fertig.append(el('p', 'leise',
'DogFather und die rechte Hand lesen sie – sonst niemand. '
+ 'Du hörst von uns; das kann ein paar Tage dauern.'));
const zurueck = el('a', 'knopf', 'Zurück zum Treff');
zurueck.href = 'bereich.html?b=mitmachen';
fertig.append(zurueck);
fertig.scrollIntoView({ block: 'start', behavior: 'smooth' });
} catch {
melde('Das hat nicht geklappt. Dein Entwurf ist noch da.');
knopf.disabled = false;
knopf.textContent = 'Anfrage abschicken';
}
});
})();
})();
+264
View File
@@ -0,0 +1,264 @@
/* =====================================================================
DER EINGANG FÜR DIE ANFRAGEN AUS DER COMMUNITY (17.09.2026)
Filipe: „und dan soll ich annehmen ablehnen, in warteschlange stellen
oder sofort kommunizieren können."
VIER WEGE, EINE ANTWORT. Die Reihenfolge kommt vom Server und ist
dort begründet: „Reden" steht vorne, weil die häufigste richtige
Antwort auf eine Bewerbung eine Rückfrage ist und nicht eine
Entscheidung. Stünde „Annehmen" oben, würde es geklickt.
„SOFORT KOMMUNIZIEREN" HEISST HIER: EIN SATZ, DEN SIE LIEST.
Nicht ein Chat -- ein Zugang aus der Community steht ausdrücklich
nicht in den Chatlisten (AUSSEN_ROLLEN, 17.09.). Es gibt also genau
einen Weg, dieser Person etwas zu sagen: den Satz, der zur
Entscheidung gehört. Er erscheint auf ihrer eigenen Seite. Deshalb
ist er bei „Reden" und „Ablehnen" Pflicht -- welche das sind, steht
im Katalog und nicht hier.
WAS DIESE SEITE NICHT TUT: sortieren lassen, filtern, suchen. Die
Reihenfolge ist die Arbeitsanweisung (neu zuerst, dann was liegt),
und wer sie umstellen kann, arbeitet irgendwann die bequemen zuerst
ab. Bei zwanzig Anfragen im Jahr ist Suchen ohnehin keine Frage.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
const melde = (t) => {
const f = $('fehler');
if (f) { f.textContent = t || ''; f.hidden = !t; }
};
const el = (art, klasse, text) => {
const k = document.createElement(art);
if (klasse) k.className = klasse;
if (text != null) k.textContent = text;
return k;
};
const ZUSTAND_WORT = {
neu: 'Neu', gespraech: 'Im Gespräch', warteschlange: 'Warteliste',
angenommen: 'Angenommen', abgelehnt: 'Abgelehnt',
};
/* WIE LANGE SIE SCHON LIEGT, in Worten. Eine Zahl allein sagt
nichts -- „seit 23 Tagen" sagt, dass etwas schiefläuft. */
const liegtSeit = (tage) => {
if (tage <= 0) return 'heute gekommen';
if (tage === 1) return 'seit gestern';
return 'liegt seit ' + tage + ' Tagen';
};
let daten = null;
const zeichnen = () => {
/* ---------- Die Zahlen oben ---------- */
const z = $('zahlen');
z.textContent = '';
z.hidden = false;
const offen = (daten.zahlen.neu || 0) + (daten.zahlen.gespraech || 0)
+ (daten.zahlen.warteschlange || 0);
const paare = [
['neu', 'Neu', daten.zahlen.neu || 0],
['offen', 'Offen insgesamt', offen],
['dabei', 'Angenommen', daten.zahlen.angenommen || 0],
];
for (const [art, schild, wert] of paare) {
const kasten = el('div', 'bilanz-zahl');
kasten.dataset.art = art;
kasten.append(el('strong', 'bilanz-zahl__wert', String(wert)));
kasten.append(el('span', 'bilanz-zahl__schild', schild));
z.append(kasten);
}
/* ---------- Die Anfragen ---------- */
const liste = $('liste');
liste.textContent = '';
if (!daten.bewerbungen.length) {
const leer = el('div', 'bw-leer');
leer.append(el('p', 'bw-leer__wort', 'Noch hat sich niemand gemeldet.'));
leer.append(el('p', 'leise',
'Der Weg dorthin steht im Treff unter „Mitmachen“. Wenn dort '
+ 'nichts ankommt, liegt es fast nie an den Leuten, sondern '
+ 'daran, dass niemand weiß, dass es geht – sag es im Stream.'));
liste.append(leer);
return;
}
for (const b of daten.bewerbungen) {
const karte = el('article', 'bw-karte');
karte.dataset.zustand = b.zustand;
/* ---- Kopf ---- */
const kopf = el('header', 'bw-karte__kopf');
const links = el('div', 'bw-karte__wer');
links.append(el('h2', 'bw-karte__name', b.nenn_name || 'ohne Namen'));
const unter = el('p', 'bw-karte__unter');
unter.append(document.createTextNode(liegtSeit(b.tage)));
/* DER KONTONAME STEHT DABEI, aber klein und dahinter. Er ist
nötig, um die Person wiederzufinden -- gemeint ist sie aber
mit dem Namen, den sie selbst genannt hat. */
if (b.konto_name && b.konto_name !== b.nenn_name) {
unter.append(document.createTextNode(' · Zugang: ' + b.konto_name));
}
links.append(unter);
kopf.append(links);
kopf.append(el('span', 'bw-marke', ZUSTAND_WORT[b.zustand] || b.zustand));
karte.append(kopf);
/* ---- Was schon entschieden wurde ---- */
if (b.zustand !== 'neu' && (b.grund || b.von_name)) {
const alt = el('div', 'bw-karte__frueher');
if (b.grund) alt.append(el('p', 'bw-karte__grund', b.grund));
if (b.von_name) {
alt.append(el('p', 'leise', 'entschieden von ' + b.von_name));
}
karte.append(alt);
}
/* ---- Die Antworten, nach Gruppen ---- */
const bogen = el('div', 'bw-karte__bogen');
for (const g of daten.gruppen) {
const fragen = daten.fragen.filter(
(f) => f.gruppe === g.schluessel && b.antworten[f.schluessel]);
if (!fragen.length) continue;
bogen.append(el('h3', 'bw-karte__gruppe', g.name));
for (const f of fragen) {
const paar = el('div', 'bw-antwort');
paar.append(el('p', 'bw-antwort__frage', f.titel));
paar.append(el('p', 'bw-antwort__text', b.antworten[f.schluessel]));
bogen.append(paar);
}
}
/* WAS NICHT BEANTWORTET WURDE, WIRD GENANNT -- nicht verschwiegen.
Eine freiwillige Frage, die jemand überspringt, ist selbst eine
Auskunft; sie einfach wegzulassen versteckt sie. */
const offenFragen = daten.fragen.filter((f) => !b.antworten[f.schluessel]);
if (offenFragen.length) {
bogen.append(el('p', 'bw-karte__ohne',
'Ohne Antwort: ' + offenFragen.map((f) => f.titel).join(' · ')));
}
karte.append(bogen);
/* ---- Die Entscheidung ---- */
const wahl = el('div', 'bw-wahl');
const schild = el('p', 'bw-wahl__schild', 'Was passiert damit?');
wahl.append(schild);
const feldkasten = el('div', 'bw-wahl__feld');
feldkasten.hidden = true;
const feldSchild = el('label', 'bw-wahl__feldschild');
const feld = document.createElement('textarea');
feld.className = 'bw-wahl__eingabe';
feld.rows = 3;
feld.maxLength = 600;
feld.id = 'grund-' + b.id;
feldSchild.htmlFor = feld.id;
feldkasten.append(feldSchild, feld);
const reihe = el('div', 'bw-wahl__knoepfe');
let gewaehlt = null;
const senden = el('button', 'knopf', 'Übernehmen');
senden.type = 'button';
senden.hidden = true;
for (const w of daten.wege) {
const k = el('button', 'bw-weg', w.name);
k.type = 'button';
k.dataset.weg = w.schluessel;
k.setAttribute('aria-pressed', 'false');
k.title = w.text || '';
if (w.schluessel === b.zustand) k.disabled = true;
k.addEventListener('click', () => {
gewaehlt = w;
for (const a of reihe.querySelectorAll('.bw-weg')) {
a.setAttribute('aria-pressed', String(a === k));
}
/* OB EIN SATZ NÖTIG IST, sagt der Weg selbst -- die Regel
kommt aus derselben Antwort, die der Server prüft. */
feldkasten.hidden = false;
feldSchild.textContent = w.nachrichtSchild || 'Ein Satz dazu';
feld.placeholder = w.nachricht === 'pflicht' ? 'nötig' : 'kann leer bleiben';
senden.hidden = false;
senden.textContent = w.name + ' – übernehmen';
});
reihe.append(k);
}
senden.addEventListener('click', async () => {
if (!gewaehlt) return;
melde('');
const grund = feld.value.trim();
if (gewaehlt.nachricht === 'pflicht' && grund.length < (daten.nachricht_min || 10)) {
melde('Dazu gehört ein Satz – er steht danach auf ihrer Seite.');
feld.focus();
return;
}
senden.disabled = true;
senden.textContent = 'einen Moment …';
try {
const a = await fetch('/workspace/api/bewerbung/' + b.id + '/weg', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ weg: gewaehlt.schluessel, grund }),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) {
melde(d.fehler || 'Das hat nicht geklappt.');
senden.disabled = false;
senden.textContent = gewaehlt.name + ' – übernehmen';
return;
}
/* NACH EINER ANNAHME steht der Weg zum Talent da -- und zwar
als Angebot, nicht als Sprung. Wer gerade fünf Anfragen
durchgeht, will nicht nach der ersten auf einer anderen
Seite landen. */
await laden();
if (d.weiter) {
const hin = el('p', 'bw-hinweis');
const a2 = el('a', 'schritt', 'Bei den Talenten ansehen');
a2.href = d.weiter.replace('/workspace/', '');
hin.append(document.createTextNode('Daraus wurde ein Talent. '));
hin.append(a2);
$('liste').prepend(hin);
}
} catch {
melde('Das hat nicht geklappt.');
senden.disabled = false;
senden.textContent = gewaehlt.name + ' – übernehmen';
}
});
wahl.append(reihe, feldkasten, senden);
karte.append(wahl);
liste.append(karte);
}
};
const laden = async () => {
let a;
try {
a = await fetch('/workspace/api/bewerbungen', { credentials: 'same-origin' });
} catch {
melde('Die Anfragen sind gerade nicht erreichbar.');
return;
}
if (a.status === 401) { location.assign('/workspace/'); return; }
if (a.status === 404 || a.status === 403) {
$('ladezeile').hidden = true;
melde('Diese Seite ist für DogFather und die rechte Hand.');
return;
}
if (!a.ok) { melde('Die Anfragen sind gerade nicht erreichbar.'); return; }
daten = await a.json();
$('ladezeile').hidden = true;
zeichnen();
};
laden();
})();
+55 -1
View File
@@ -646,7 +646,61 @@
} catch { /* umgeleitet */ }
}
/* DIE ANFRAGEN -- nur die Zahl, nicht der Inhalt (17.09.2026).
Eine eigene, winzige Auskunft. Den vollen Eingang zu laden hiesse,
vertrauliche Saetze in eine Seite zu holen, die sie gar nicht
anzeigt -- im Zwischenspeicher des Browsers landen sie trotzdem.
SCHWEIGT BEI 404. Wer diese Seite sieht, darf die Anfragen zwar in
aller Regel auch sehen; aber die Rechtetafel ist umstellbar, und
ein Kasten, der dann ins Leere fuehrt, ist schlechter als keiner. */
async function anfragenZeigen() {
const kasten = $('anfragen-tor');
if (!kasten) return;
let d;
try {
const a = await hole('/workspace/api/bewerbungen-zahl');
if (!a.ok) return;
d = await a.json();
} catch { return; }
kasten.textContent = '';
kasten.hidden = false;
if (d.neu > 0) kasten.dataset.neu = 'ja'; else delete kasten.dataset.neu;
const text = document.createElement('div');
const wort = document.createElement('p');
wort.className = 'bw-tor__wort';
/* DIE ZAHL STEHT IM SATZ, nicht als nackte Ziffer daneben. „2 neue
Anfragen" sagt, was zu tun ist; eine 2 in einem Kreis sagt es
nicht. */
wort.textContent = d.neu > 0
? (d.neu === 1 ? 'Eine neue Anfrage wartet.' : d.neu + ' neue Anfragen warten.')
: (d.offen > 0
? (d.offen === 1 ? 'Eine Anfrage ist noch offen.' : d.offen + ' Anfragen sind noch offen.')
: 'Keine offenen Anfragen.');
text.append(wort);
const leise = document.createElement('p');
leise.className = 'leise';
leise.textContent = 'Wer sich selbst gemeldet hat. Nimmst du an, '
+ 'entsteht daraus hier ein Talent auf der Stufe „Angesprochen“.';
text.append(leise);
const hin = document.createElement('a');
hin.className = d.neu > 0 ? 'knopf' : 'schritt';
hin.href = 'bewerbungen.html';
hin.textContent = d.neu > 0 ? 'Ansehen' : 'Anfragen öffnen';
kasten.append(text, hin);
}
(async () => {
try { await ichHolen(); await laden(); } catch { /* umgeleitet */ }
try {
await ichHolen();
await laden();
await anfragenZeigen();
} catch { /* umgeleitet */ }
})();
})();
+12 -12
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -291,10 +291,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/aufgaben.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/aufgaben.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -168,10 +168,10 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/automation.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/automation.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -105,9 +105,9 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/befinden.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/befinden.js?v=202609171558" defer></script>
</body>
</html>
+30 -17
View File
@@ -12,20 +12,24 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609171558" />
<!-- Wegen des Blocks „Willst du dazugehoeren?" auf dem Brett
„Mitmachen". Eine eigene Kopie der Regel hier waere die zweite
Stelle, an der dieselbe Sache anders aussieht. -->
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -188,6 +192,15 @@
Die Regel dahinter: Ein WERKZEUG (fuer das Team) draengt
nie eine ANTWORT (fuer alle) nach unten. -->
<section id="countdown" class="countdown" hidden></section>
<!-- DER NAECHSTE SCHRITT. Was hier steht, entscheidet der Server --
er kennt die Rolle, und Rollennamen stehen in keiner Datei, die
jeder herunterladen kann. Derzeit genau eine Stelle: der Weg zur
Anfrage auf dem Brett "Mitmachen".
UEBER DEM FILTER, weil es eine ANTWORT ist und kein Werkzeug --
dieselbe Regel, die den Countdown nach oben gebracht hat. -->
<section id="weiter" class="bw-tor" hidden></section>
<div class="filter" id="filter"></div>
<!-- Fertige Punkte zum Uebernehmen -- nur in Bereichen, fuer die es
@@ -224,14 +237,14 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/checkliste.js?v=202609171416" defer></script>
<script src="assets/js/ampel.js?v=202609171416" defer></script>
<script src="assets/js/schulung.js?v=202609171416" defer></script>
<script src="assets/js/vorlagen.js?v=202609171416" defer></script>
<script src="assets/js/bereich.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/checkliste.js?v=202609171558" defer></script>
<script src="assets/js/ampel.js?v=202609171558" defer></script>
<script src="assets/js/schulung.js?v=202609171558" defer></script>
<script src="assets/js/vorlagen.js?v=202609171558" defer></script>
<script src="assets/js/bereich.js?v=202609171558" defer></script>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Anfrage · Der Treff</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
<div class="schleier" aria-hidden="true"></div>
<header class="kopfleiste">
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.5 5.5 8 12l6.5 6.5" />
</svg>
<span class="zurueck-knopf__text">Zurück</span>
</button>
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Der&nbsp;Treff</a> · <span id="marke-was">Anfrage</span></span></p>
<div class="kopfleiste__rechts">
<span class="wer" id="wer">…</span>
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
</div>
</header>
<main class="inhalt">
<!-- ==================================================================
DIE ANFRAGE AUS DER COMMUNITY (17.09.2026)
Filipe: „ich will dass die leute sich da anonym bei mir melden
können und eine anfrage machen können. ich will dass es
aber profissionel gemacht ist bitte wie ein fragebogen."
VERTRAULICH, NICHT NAMENLOS - seine Entscheidung. Es gibt keine
Selbstanmeldung im Treff; jeder Zugang kommt von DogFather selbst.
Eine wirklich namenlose Anfrage wäre also nicht anonymer, sondern
nur schlechter: Man könnte niemandem antworten, und aus einer
Anfrage würde nie ein Gespräch.
„Anonym" heißt hier deshalb: NIEMAND AUS DER COMMUNITY SIEHT SIE.
Nicht die anderen Mitglieder, nicht die Modis - nur DogFather und
die rechte Hand. Und das steht auf dieser Seite, bevor jemand
anfängt zu tippen, statt im Kleingedruckten darunter.
============================================================== -->
<section class="kopf-zeile">
<div>
<p class="marke">Mitmachen</p>
<!-- DER NAME DER ROLLE STEHT NICHT IN DIESER DATEI.
Sie ist ohne Anmeldung aus dem offenen Netz lesbar (gemessen
am 17.09.2026: jede Datei unter /workspace/ antwortet mit
200). Der Server schickt das Wort in seiner Antwort mit --
dieselbe Regel wie bei den Treff-Regeln und im Chat.
Was hier steht, ist die ehrliche Ueberschrift, falls die
Antwort ausbleibt: eine Anfrage ist es so oder so. -->
<h1 class="titel" id="titel">Anfrage stellen</h1>
<p class="unterzeile">
Ein paar Fragen - ehrlich beantwortet ist mehr wert als
vollständig. Du kannst zwischendurch aufhören und später
weitermachen; gespeichert wird erst, wenn du abschickst.
</p>
</div>
</section>
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<p class="leise" id="ladezeile">einen Moment …</p>
<!-- Der Stand, wenn schon eine Anfrage vorliegt (oder eine Sperrfrist
läuft). Steht ganz oben: Wer hier landet und schon geschickt hat,
will als Erstes wissen, was daraus wurde - nicht das Formular
noch einmal sehen. -->
<section class="bw-stand" id="stand" hidden></section>
<form class="bw-bogen" id="bogen" hidden novalidate>
<div id="gruppen"></div>
<!-- WER DAS LIEST, steht direkt über dem Absenden-Knopf und nicht
nur oben auf der Seite. Wer hier unten ankommt, hat oben zehn
Minuten hinter sich. -->
<p class="bw-wer-liest" id="wer-liest"></p>
<div class="bw-abschluss">
<button type="submit" class="knopf" id="abschicken">Anfrage abschicken</button>
<a class="schritt" href="bereich.html?b=mitmachen">Doch nicht</a>
</div>
</form>
</main>
<!-- Der Fuß gehört zu jeder Seite - siehe pruef-css-klassen. -->
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/auskunft.js?v=202609171558" defer></script>
<script src="assets/js/bewerben.js?v=202609171558" defer></script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Anfragen · Nachwuchs</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
<div class="schleier" aria-hidden="true"></div>
<header class="kopfleiste">
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.5 5.5 8 12l6.5 6.5" />
</svg>
<span class="zurueck-knopf__text">Zurück</span>
</button>
<p class="marke"><span class="marke__text"><a class="zurueck" href="talente.html">Talente</a> · Anfragen</span></p>
<div class="kopfleiste__rechts">
<span class="wer" id="wer">…</span>
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
</div>
</header>
<main class="inhalt inhalt--breit">
<!-- ==================================================================
DER EINGANG FÜR DIE ANFRAGEN AUS DER COMMUNITY (17.09.2026)
Filipe: „und dan soll ich annehmen ablehnen, in warteschlange
stellen oder sofort kommunizieren können. und wenn ich annehme dan
soll diese person automatisch zu talente kategorie weiter geleitet
werden."
HIER STEHEN SÄTZE, DIE JEMAND ÜBER SICH SELBST GESCHRIEBEN HAT,
im Vertrauen darauf, dass nur zwei Menschen sie lesen. Diese Seite
gehört DogFather und der rechten Hand -- durchgesetzt in der
Rechtetafel UND in workspace-bewerbung.js.
Deshalb steht im Protokoll auch kein Inhalt, sondern nur die
Nummer: Das Protokoll lesen mehr Leute als die Anfrage.
============================================================== -->
<section class="kopf-zeile">
<div>
<p class="marke">Entwicklung &amp; Nachwuchs</p>
<h1 class="titel">Anfragen</h1>
<p class="unterzeile">
Wer sich selbst gemeldet hat. Nimmst du an, entsteht daraus ein
Eintrag bei den Talenten – auf der Stufe „Angesprochen“, denn wer
sich meldet, hat die erste Frage schon beantwortet.
</p>
</div>
</section>
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- Die Zahlen. Nicht Schmuck, sondern die Arbeitsanweisung dieser
Seite: Was neu ist, steht links und wird zuerst gelesen. -->
<section class="bw-zahlen" id="zahlen" hidden></section>
<p class="leise" id="ladezeile">einen Moment …</p>
<div id="liste"></div>
</main>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/auskunft.js?v=202609171558" defer></script>
<script src="assets/js/bewerbungen.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -89,11 +89,11 @@
</main>
<script src="assets/js/ki.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/calls.js?v=202609171416" defer></script>
<script src="assets/js/ki.js?v=202609171558" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/calls.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,26 +12,26 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -341,10 +341,10 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609171416"></script>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416"></script>
<script src="assets/js/glocke.js?v=202609171416"></script>
<script src="assets/js/chat.js?v=202609171416"></script>
<script src="assets/js/bereiche.js?v=202609171558"></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558"></script>
<script src="assets/js/glocke.js?v=202609171558"></script>
<script src="assets/js/chat.js?v=202609171558"></script>
</body>
</html>
+15 -15
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/content.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/content.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -179,12 +179,12 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/checkliste.js?v=202609171416" defer></script>
<script src="assets/js/vorlagen.js?v=202609171416" defer></script>
<script src="assets/js/content.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/checkliste.js?v=202609171558" defer></script>
<script src="assets/js/vorlagen.js?v=202609171558" defer></script>
<script src="assets/js/content.js?v=202609171558" defer></script>
</body>
</html>
+5 -5
View File
@@ -36,15 +36,15 @@
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
die eine Seite, auf der man das Umfaerben sieht. -->
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609171558" />
</head>
<body class="gate gate--crew">
@@ -339,6 +339,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609171416" defer></script>
<script src="assets/js/gate.js?v=202609171558" defer></script>
</body>
</html>
+4 -4
View File
@@ -22,25 +22,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609171416",
"src": "/assets/img/app-symbole/crew-192.png?v=202609171558",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609171416",
"src": "/assets/img/app-symbole/crew-512.png?v=202609171558",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171416",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171558",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171416",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171558",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -108,10 +108,10 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/dateien.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/dateien.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -102,9 +102,9 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/entwicklung.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/entwicklung.js?v=202609171558" defer></script>
</body>
</html>
+4 -4
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
</head>
<body class="gate">
@@ -401,6 +401,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609171416" defer></script>
<script src="assets/js/gate.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -377,11 +377,11 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/kalender.js?v=202609171416" defer></script>
<script src="assets/js/kalender-abo.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/kalender.js?v=202609171558" defer></script>
<script src="assets/js/kalender-abo.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -444,10 +444,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609171416"></script>
<script src="assets/js/bereiche.js?v=202609171416"></script>
<script src="assets/js/kopf.js?v=202609171416"></script>
<script src="assets/js/glocke.js?v=202609171416"></script>
<script src="assets/js/leistung.js?v=202609171416"></script>
<script src="assets/js/wahl.js?v=202609171558"></script>
<script src="assets/js/bereiche.js?v=202609171558"></script>
<script src="assets/js/kopf.js?v=202609171558"></script>
<script src="assets/js/glocke.js?v=202609171558"></script>
<script src="assets/js/leistung.js?v=202609171558"></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -423,10 +423,10 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/personen.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/personen.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -214,11 +214,11 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/steckbrief.js?v=202609171416" defer></script>
<script src="assets/js/profil.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/steckbrief.js?v=202609171558" defer></script>
<script src="assets/js/profil.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -134,9 +134,9 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/rechte.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/rechte.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/report.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/report.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -106,11 +106,11 @@
</main>
<script src="assets/js/ki.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/report.js?v=202609171416" defer></script>
<script src="assets/js/ki.js?v=202609171558" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/report.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -173,10 +173,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/scouting.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/scouting.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,21 +12,21 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -674,10 +674,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/start.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/start.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -85,11 +85,11 @@
</main>
<script src="assets/js/ki.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/startcheck.js?v=202609171416" defer></script>
<script src="assets/js/ki.js?v=202609171558" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/startcheck.js?v=202609171558" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -146,11 +146,11 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/auskunft.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/steckbrief.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/auskunft.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/steckbrief.js?v=202609171558" defer></script>
</body>
</html>
+29 -12
View File
@@ -7,14 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609171558" />
<!-- Wegen des Kastens „Anfragen“ ueber der Liste. Eine eigene Kopie
der Regel hier waere die zweite Stelle, an der dieselbe Sache
anders aussieht. -->
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -50,6 +54,19 @@
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- DIE ANFRAGEN GEHOEREN AN DEN ANFANG DIESES TRICHTERS.
Wer sich selbst meldet, ist ein Kandidat wie jeder andere -- nur
dass er die erste Frage ("besteht ueberhaupt Interesse?") schon
beantwortet hat. Deshalb steht der Weg dorthin auf DIESER Seite
und nicht als eigene Kachel daneben: Sonst waere aus einem Weg
zwei Seiten geworden, die man abwechselnd aufmacht.
Der Kasten steht ueber der Liste, weil eine unbeantwortete
Anfrage dringender ist als ein Kandidat, der seit Wochen
beobachtet wird. -->
<section id="anfragen-tor" class="bw-tor" hidden></section>
<!-- ==================================================================
HIER STEHEN NAMEN VON MENSCHEN, DIE NICHTS DAVON WISSEN.
@@ -94,9 +111,9 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/talente.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/talente.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/team.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/team.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -110,10 +110,10 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/team.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/team.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/team.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/team.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
@@ -84,9 +84,9 @@
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
es niemandem auf, und deshalb prueft pruef-css-klassen die
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/teamlage.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/teamlage.js?v=202609171558" defer></script>
</body>
</html>
+11 -11
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -112,9 +112,9 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/teilen.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/teilen.js?v=202609171558" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -126,9 +126,9 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/treff-moderation.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/treff-moderation.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -7,18 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -211,10 +211,10 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/auskunft.js?v=202609171416" defer></script>
<script src="assets/js/treff-regeln.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/auskunft.js?v=202609171558" defer></script>
<script src="assets/js/treff-regeln.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,22 +12,22 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -149,11 +149,11 @@
</main>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/uebersicht.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/uebersicht.js?v=202609171558" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171416" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171416" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171416" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609171558" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609171558" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/start.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609171558" />
<link rel="stylesheet" href="assets/css/module.css?v=202609171558" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609171416" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609171558" />
</head>
<body class="start">
@@ -190,10 +190,10 @@
</main>
<script src="assets/js/wahl.js?v=202609171416" defer></script>
<script src="assets/js/bereiche.js?v=202609171416" defer></script>
<script src="assets/js/kopf.js?v=202609171416" defer></script>
<script src="assets/js/glocke.js?v=202609171416" defer></script>
<script src="assets/js/wissen.js?v=202609171416" defer></script>
<script src="assets/js/wahl.js?v=202609171558" defer></script>
<script src="assets/js/bereiche.js?v=202609171558" defer></script>
<script src="assets/js/kopf.js?v=202609171558" defer></script>
<script src="assets/js/glocke.js?v=202609171558" defer></script>
<script src="assets/js/wissen.js?v=202609171558" defer></script>
</body>
</html>