Mit "@" jemanden im Chat ansprechen

Filipe: "ich will das man die leute mit @ markieren kann im chat."

MARKIEREN IST KEINE FARBE, SONDERN EINE ANSPRACHE. Sie funktioniert
nur, wenn drei Dinge zusammen stimmen: Der Richtige ist gemeint, er
MERKT es, und man sieht es im Satz. Fehlt eines davon, ist es eine
Attrappe -- und zwar eine, die gut aussieht.

WER GEMEINT IST, ENTSCHEIDET DER SERVER. Der Browser schickt nur Text.
Damit wirkt ein von Hand getipptes "@VanVan" genauso wie eines aus der
Auswahlliste, und niemand kann jemanden ansprechen, der gar nicht im
Raum sitzt -- auch nicht ueber die Schnittstelle.

Die Regeln stehen in server/chat-erwaehnung.js, jede mit Begruendung.
Zwei davon sind keine Feinheit, sondern der Unterschied zwischen
brauchbar und laestig:

  * Das Zeichen VOR dem "@" darf kein Buchstabe sein. Sonst piepst
    jede E-Mail-Adresse im Chat jemanden an
    ("[email protected]").
  * Das Zeichen DANACH auch nicht, und der laengste Name gewinnt.
    Sonst spricht "@Tilikum" Tili an.

MAN MERKT ES AUCH OHNE OFFENEN RAUM. Eine Zahl in der Gespraechsliste
sagt "hier ist etwas" -- nicht, ob es an dich war. Wer morgens vier
Raeume mit Zahlen sieht, macht den lautesten zuerst auf, und dort
steht selten das, was auf ihn wartet. Jetzt steht ein "@" daneben.

UND DIE MELDUNG AUFS HANDY SAGT ES: "VanVan hat dich erwaehnt" statt
"Nachricht von VanVan". Als EIGENE Art, die sich getrennt abschalten
laesst -- wer den lauten Chat stumm stellt, will trotzdem wissen, wenn
ihn jemand direkt anspricht. Ohne Ausnahme von der Ruhezeit: Ein Anruf
wartet auf eine Antwort, ein "@Anna" nicht.

DIE AUSWAHL BEIM TIPPEN loest drei Dinge, die man sonst selbst wissen
muesste: wie die Person genau heisst, wer ueberhaupt im Raum ist, und
ob man sich vertippt hat. Tastatur zuerst (Pfeile, Enter, Tab, Escape).
Sie erzwingt nichts -- wer weitertippt, schreibt einfach weiter.

ZWEI RECHNUNGEN, EINE ANTWORT: Der Browser braucht dieselbe Aufloesung,
um sofort hervorzuheben. Genau dort laufen Dinge auseinander, und dann
haette die Seite jemanden hervorgehoben, den niemand benachrichtigt
hat. pruef-erwaehnung LIEST die Browserfassung aus der Datei und fuehrt
sie aus -- ein Nachbau wuerde pruefen, ob ich zweimal dasselbe
schreiben kann.

---- DABEI GEFUNDEN: "gelesen bis 999999" ----------------------------

Der Server nahm fuer den Lesestand JEDE ganze Zahl an. Wer einmal eine
Zahl hinter allem Vorhandenen schickte, sah in diesem Gespraech nie
wieder einen Zaehler und nie wieder ein @-Zeichen: Alles Kuenftige galt
als gelesen, bevor es geschrieben war. Das faellt niemandem als
Zusammenhang auf -- man merkt nur, dass "die Benachrichtigungen nicht
gehen".

Der Browser schickt heute immer die letzte gesehene Nummer, ist also
nicht der Grund. Aber eine Grenze, die nur davon lebt, dass der
Aufrufer sich benimmt, ist keine. Jetzt wird auf die juengste
Nachricht des Raums begrenzt -- abgeleitet, nicht geraten.

Gefunden hat es meine eigene Pruefung: Sie setzte zum Aufraeumen 99999
und wunderte sich danach, warum das frische "@" nicht leuchtete.

WOHER MAN ERFAEHRT, DASS ES DAS GIBT: Im Chat gibt es keinen
Hilfeknopf. Der Hinweis steht deshalb im Leersatz einer neuen Gruppe --
dort, wo man beim ersten Oeffnen ohnehin hinsieht, und nur ab drei
Leuten. In einem Zweiergespraech waere er Ballast.

GEPRUEFT: pruef-erwaehnung 67/0 (neu) -- 13 Regelfaelle ohne Server,
der Weg durch die Schnittstelle, das Zeichen in der Liste samt
Gegenprobe bei jemandem, der dabei aber nicht gemeint war, beide
Aufloesungen nebeneinander, und der ganze Ablauf am echten Bildschirm
bis "Enter waehlt und schickt NICHT ab".
Dazu pruef-chat 48/0, pruef-chat-optik 36/0, pruef-treffchat 110/0,
pruef-chat-kanaele 79/0, pruef-glocke 31/0, pruef-push 24/0,
pruef-anruf-klingelt, pruef-css-klassen, pruef-meldungen,
pruef-nachfrage, pruef-tippziele.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-20 17:54:16 +02:00
co-authored by Claude Opus 5
parent 409ed551f3
commit 2a26a49f91
44 changed files with 1959 additions and 534 deletions
+152
View File
@@ -0,0 +1,152 @@
/* =====================================================================
WER IST MIT "@" GEMEINT? (20.09.2026)
Filipe: "ich will das man die leute mit @ markieren kann im chat."
---------------------------------------------------------------------
WARUM DAS EINE EIGENE DATEI IST
Die Auflösung "Text -> Menschen" wird an DREI Stellen gebraucht und
muss überall dasselbe ergeben:
1. beim Schreiben -- wer bekommt eine Benachrichtigung,
2. im Browser -- welcher Teil des Satzes wird hervorgehoben,
3. in der Prüfung -- ohne dafür einen Server hochzufahren.
Stünde sie in workspace-chat.js, müsste die Prüfung die halbe
Anwendung laden, um einen Satz zu zerlegen. Und der Browser hätte
ohnehin eine zweite, eigene Fassung -- also genau die Stelle, an der
zwei Rechnungen irgendwann auseinanderlaufen und die Seite jemanden
farbig hervorhebt, den der Server nie benachrichtigt hat.
DIE DATEI HAT DESHALB KEINE ABHÄNGIGKEITEN. Kein Datenbankzugriff,
kein Import. Herein kommt ein Text und eine Liste von Menschen,
heraus kommt, wer gemeint ist. Der Browser bekommt dieselbe Logik in
`chat.js` -- bewusst als kurze, gleichlautende Fassung, die
pruef-erwaehnung.mjs Zeile für Zeile gegen diese hier prüft.
---------------------------------------------------------------------
DIE REGELN, UND WARUM JEDE EINZELNE NÖTIG IST
(1) NUR WER IM RAUM IST.
"@Marina" in einem Gespräch, in dem Marina nicht sitzt, ist kein
Ansprechen, sondern ein Wort über sie. Sie bekäme sonst eine
Meldung zu einem Gespräch, das sie nicht öffnen darf -- eine
Benachrichtigung, die auf eine verschlossene Tür führt.
(2) DAS ZEICHEN DAVOR DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN.
Ohne diese Regel wird aus "[email protected]" ein
Ansprechen von Dogfather -- jede E-Mail-Adresse im Chat würde
jemanden anpiepsen. Gemessen an einer echten Adresse, siehe
Prüfung.
(3) DAS ZEICHEN DANACH DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN.
Sonst wäre "@Tili" in "@Tilikum" enthalten. Satzzeichen dagegen
schon: "@Tili, kannst du?" und "@Tili!" sind normale Sätze.
(4) DER LÄNGSTE PASSENDE NAME GEWINNT.
Gäbe es "Van" und "VanVan", träfe die kurze Fassung zuerst und
ließe "Van" stehen. Heute hat niemand einen Namen, der im
Namen eines anderen steckt -- aber das ist eine Tatsache von
heute, kein Gesetz, und sie ändert sich mit dem nächsten Zugang.
(5) GROSS UND KLEIN EGAL.
Wer "@vanvan" tippt, meint VanVan. Auf einer Handytastatur
entscheidet die Autokorrektur mit, nicht der Mensch.
(6) JEDE PERSON HÖCHSTENS EINMAL.
Dreimal "@Anna" in einem Satz ist trotzdem eine Ansprache.
---------------------------------------------------------------------
WAS ES BEWUSST NICHT GIBT
KEIN "@alle". Es wäre in fünf Minuten gebaut und ist der zuverlässigste
Weg, dass alle die Benachrichtigungen abschalten -- und dann kommt
auch die an, die wirklich für einen bestimmten Menschen gedacht war.
Wenn Filipe es ausdrücklich will, gehört dazu eine Entscheidung, WER
es benutzen darf; das ist eine Frage an ihn, keine, die ich hier
beantworte.
AUFRUF (auch aus einer Prüfung heraus):
import { erwaehnungenFinden } from "./chat-erwaehnung.js";
===================================================================== */
/** Ein Zeichen, das einen Namen fortsetzen könnte.
*
* BUCHSTABE ODER ZIFFER, und zwar auch mit Umlaut oder Akzent:
* `\p{L}` statt `[a-z]`. Mit `[a-zA-Z0-9]` wäre "@Jose" in "@José"
* ein Treffer gewesen -- das "é" hätte als Grenze gezählt. */
const WORTZEICHEN = /[\p{L}\p{N}_]/u;
/** Steht an dieser Stelle ein Wortzeichen?
* Außerhalb des Textes (Anfang/Ende) ist die Antwort NEIN -- ein
* Satzanfang ist eine Grenze wie jede andere. */
function wortzeichenBei(text, i) {
if (i < 0 || i >= text.length) return false;
return WORTZEICHEN.test(text[i]);
}
/**
* Wer ist in diesem Text mit "@" angesprochen?
*
* @param {string} text Die geschriebene Nachricht.
* @param {Array<{id:number,name:string}>} leute
* Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann
* nicht gemeint sein -- siehe Regel (1) oben.
* @returns {Array<{id:number, name:string, von:number, bis:number}>}
* Je Treffer die Person und die Stelle im Text (`von` bis
* `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die
* Stellen braucht der Browser zum Hervorheben; der Server
* benutzt nur `id`. Sortiert nach Vorkommen im Text.
*/
export function erwaehnungenFinden(text, leute) {
const satz = String(text || "");
if (!satz.includes("@")) return [];
/* REGEL (4): der längste Name zuerst. Einmal sortiert statt bei
jedem Treffer neu -- und auf einer Kopie, damit die Liste des
Aufrufers unberührt bleibt. */
const nachLaenge = (Array.isArray(leute) ? leute.slice() : [])
.filter((p) => p && p.id != null && String(p.name || "").trim())
.sort((a, b) => String(b.name).length - String(a.name).length);
if (!nachLaenge.length) return [];
const klein = satz.toLowerCase();
const treffer = [];
const schonDa = new Set(); // Regel (6)
for (let i = klein.indexOf("@"); i !== -1; i = klein.indexOf("@", i + 1)) {
/* REGEL (2): Was steht vor dem "@"? */
if (wortzeichenBei(satz, i - 1)) continue;
for (const person of nachLaenge) {
const name = String(person.name);
const nk = name.toLowerCase();
if (!klein.startsWith(nk, i + 1)) continue;
/* REGEL (3): Was steht hinter dem Namen? */
if (wortzeichenBei(satz, i + 1 + name.length)) continue;
/* Die Stelle wird IMMER gemeldet, auch wenn dieselbe Person
schon einmal vorkam: Der Browser soll jedes "@Anna" im Satz
hervorheben, nicht nur das erste. Nur die ID-Liste für die
Benachrichtigung wird entdoppelt -- das macht `ideeVon()`
weiter unten. */
treffer.push({ id: person.id, name, von: i, bis: i + 1 + name.length });
schonDa.add(person.id);
break; // Regel (4): der Längste hat gewonnen
}
}
return treffer;
}
/**
* Nur die Personennummern, jede einmal -- das, was der Server zum
* Benachrichtigen braucht.
*
* ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht
* `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten
* gleich zu schreiben, und an einem davon irgendwann anders.
*/
export function erwaehnteIds(text, leute) {
return [...new Set(erwaehnungenFinden(text, leute).map((t) => t.id))];
}
+612
View File
@@ -0,0 +1,612 @@
/* =====================================================================
pruef-erwaehnung.mjs — "@Name" im Chat
Filipe, 20.09.2026: "ich will das man die leute mit @ markieren
kann im chat."
---------------------------------------------------------------------
WAS HIER GEPRÜFT WIRD, UND WARUM GERADE DAS
Markieren ist keine Farbe, sondern eine Ansprache. Es funktioniert
nur, wenn DREI Dinge zusammen stimmen:
1. Der Richtige ist gemeint. -> Abschnitt 1 und 3
2. Er merkt es. -> Abschnitt 2 und 4
3. Man sieht es im Satz. -> Abschnitt 5 (echter Browser)
Stimmt eines davon nicht, ist die ganze Funktion eine Attrappe --
und zwar eine, die gut aussieht. Deshalb hört diese Prüfung nicht
beim "wird gespeichert" auf.
---------------------------------------------------------------------
DIE GEFÄHRLICHSTE STELLE
Es gibt ZWEI Auflösungen desselben Problems: eine im Server
(chat-erwaehnung.js) und eine im Browser (chat.js). Die zweite ist
nötig, weil der Browser sofort hervorheben muss, ohne zu fragen.
Zwei Rechnungen für dieselbe Frage sind genau die Stelle, an der
etwas auseinanderläuft -- dann hebt die Seite jemanden hervor, den
der Server nie benachrichtigt hat. Abschnitt 6 stellt beide
nebeneinander und prüft sie an denselben Fällen.
AUFRUF: node server/pruef-erwaehnung.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
import { erwaehnungenFinden, erwaehnteIds } from "./chat-erwaehnung.js";
const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = await portMussFreiSein(4426, "pruef-erwaehnung");
const ordner = mkdtempSync(join(tmpdir(), "ws-erwaehnung-"));
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 }); };
await import("./index.js");
notbremse(300_000, "pruef-erwaehnung");
await new Promise((r) => setTimeout(r, 900));
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;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* KEINE HOST-UMSCHREIBUNG, UND DAS HAT EINEN GRUND.
Der erste Anlauf meldete sich ueber HTTP mit `Host: crew.…` an und
liess den Browser auf 127.0.0.1 laufen. Beides zusammen geht nicht:
Auf 127.0.0.1 liefert der Server die AGENTUR-Anmeldewand, und dort
gibt es die Rolle "hand" gar nicht -- die Pruefung wartete 30
Sekunden auf eine Kachel, die es nie geben konnte.
Das Markieren mit "@" ist von Haus und Rolle voellig unabhaengig:
Es fragt nur, wer im RAUM sitzt. Also wird hier durchgehend die
Adresse benutzt, auf der auch der Browser landet, und es werden
Rollen genommen, die dort vorkommen. Wer die Trennung der beiden
Haeuser pruefen will, findet das in pruef-haus-trennung. */
console.log('\n=== @Name im Chat ===');
/* =====================================================================
1. DIE REGELN — ohne Server, ohne Datenbank
===================================================================== */
melde("\n=== 1. Wer ist gemeint? ===");
{
const leute = [
{ id: 1, name: "Dogfather" }, { id: 2, name: "VanVan" }, { id: 3, name: "Tili" },
{ id: 4, name: "Miss" }, { id: 5, name: "Marina" },
];
/* Jeder Fall steht für eine Regel aus chat-erwaehnung.js. Die
beiden wichtigsten sind die E-Mail-Adresse und das längere Wort:
Ohne sie piepst jede Adresse im Chat jemanden an, und "@Tilikum"
spricht Tili an. */
const faelle = [
["@VanVan kannst du kurz?", [2], "der einfache Fall"],
["@vanvan schaust du mal", [2], "klein geschrieben trifft trotzdem"],
["Hey @Tili, und @Miss auch!", [3, 4], "zwei Leute, mit Satzzeichen dahinter"],
["@Tili @Tili @Tili", [3], "dreimal derselbe zählt einmal"],
["schreib an [email protected]", [], "eine E-Mail-Adresse spricht niemanden an"],
["@Tilikum ist ein Wal", [], "ein längeres Wort ist kein Name"],
["@Missgeschick passiert", [], "ein Name im Wortinneren zählt nicht"],
["@Ghost ist nicht im Raum", [], "wer nicht im Raum sitzt, ist nicht gemeint"],
["@Marina", [5], "ganz am Ende des Satzes"],
["(@Marina)", [5], "in Klammern"],
["@VanVan-Team", [2], "ein Bindestrich beendet den Namen"],
["x@VanVan", [], "ein Buchstabe davor macht es zur Adresse"],
["Ohne at-Zeichen hier", [], "kein @ — keine Ansprache"],
];
let stimmt = 0;
for (const [text, soll, was] of faelle) {
const ist = erwaehnteIds(text, leute);
const passt = JSON.stringify(ist) === JSON.stringify(soll);
if (passt) stimmt++;
ok(passt, `${was}${passt ? "" : ` (${JSON.stringify(text)} -> ${JSON.stringify(ist)}, erwartet ${JSON.stringify(soll)})`}`);
}
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Liefe die Schleife über null
Fälle, wäre oben nichts rot -- und nichts geprüft. */
ok(stimmt === faelle.length && faelle.length >= 13,
`alle ${faelle.length} Regelfälle stimmen`);
/* DER LÄNGSTE NAME GEWINNT. Heute steckt kein Name im anderen --
das ist eine Tatsache von heute und kein Gesetz. Geprüft mit
erfundenen Namen, damit die Regel unabhängig vom Bestand hält. */
const ineinander = [{ id: 10, name: "Van" }, { id: 11, name: "VanVan" }];
ok(JSON.stringify(erwaehnteIds("@VanVan hallo", ineinander)) === "[11]",
"steckt ein Name im anderen, gewinnt der längere");
ok(JSON.stringify(erwaehnteIds("@Van hallo", ineinander)) === "[10]",
" und der kürzere trifft trotzdem, wenn er allein steht");
/* Die Stellen fürs Hervorheben. Jedes Vorkommen wird gemeldet, nicht
nur das erste -- sonst bliebe das zweite "@Anna" im Satz grau. */
const roh = "Hey @Tili und nochmal @Tili";
const st = erwaehnungenFinden(roh, leute);
ok(st.length === 2 && st.every((t) => roh.slice(t.von, t.bis) === "@Tili"),
`jedes Vorkommen bekommt eine Stelle (${st.length} Stück)`);
/* GEGENPROBE: Sie muss auch NEIN sagen können. Eine Auflösung, die
immer jemanden findet, findet nichts. */
ok(erwaehnteIds("@VanVan", []).length === 0,
"Gegenprobe: ohne Teilnehmerliste ist niemand gemeint");
}
/* =====================================================================
2. DURCH DIE SCHNITTSTELLE
===================================================================== */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const jetzt = new Date().toISOString();
let idDogi, idVan, idFunny;
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
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);
const 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;
};
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
idVan = anlegen("VanVan", "scout", "CODE-SCOU-0001");
idFunny = anlegen("Funny", "scout", "CODE-SCOU-0002");
d.close();
}
function ruf(pfad, keks, methode = "GET", koerper = null) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
...(keks ? { Cookie: keks } : {}),
...(koerper ? { "Content-Type": "application/json",
Origin: `http://127.0.0.1:${PORT}` } : {}),
},
}, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
async function anmelden(rolle, code) {
const a = await ruf("/workspace/api/anmelden", null, "POST", { rolle, code });
if (a.code !== 200) console.log(` (Anmeldung ${rolle}: ${a.code} ${a.text.slice(0, 80)})`);
return "";
}
/* Die Kekse holen wir direkt aus der Antwort -- `ruf` gibt sie nicht
zurück, also ein eigener Weg fürs Anmelden. */
function anmeldenMitKeks(rolle, code) {
return new Promise((fertig, schief) => {
const koerper = JSON.stringify({ rolle, code });
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(koerper) },
}, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({
code: antwort.statusCode, text,
keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "),
}));
});
a.on("error", schief);
a.write(koerper);
a.end();
});
}
void anmelden;
melde('\n=== 2. Ein @ geht durch die Schnittstelle ===');
const aDogi = await anmeldenMitKeks("admin", "CODE-DOGI-0001");
const aVan = await anmeldenMitKeks("scout", "CODE-SCOU-0001");
const aFunny = await anmeldenMitKeks("scout", "CODE-SCOU-0002");
ok(!!aDogi.keks && !!aVan.keks && !!aFunny.keks, "drei Zugänge sind angemeldet");
/* Eine Gruppe mit allen dreien. */
const gruppe = await ruf("/workspace/api/chat/gruppe", aDogi.keks, "POST",
{ name: "Testgruppe", mit: [idVan, idFunny] });
const raumId = json(gruppe).raum_id;
ok(Number.isInteger(raumId), `eine Gruppe mit drei Leuten steht (Raum ${raumId})`);
/* Die Teilnehmer kommen MIT der Raumliste -- die Auswahlliste im
Browser braucht deshalb keine eigene Abfrage. Genau das wird hier
festgehalten, damit niemand später eine zweite baut. */
{
const l = json(await ruf("/workspace/api/chat/raeume", aDogi.keks));
const r = (l.raeume || []).find((x) => x.id === raumId);
ok(Array.isArray(r?.teilnehmer) && r.teilnehmer.length === 3,
`die Raumliste bringt die Teilnehmer schon mit (${r?.teilnehmer?.length})`);
ok(r?.teilnehmer?.every((t) => t.id && t.name),
" mit Nummer und Namen — mehr braucht die Auswahl nicht");
}
/* Jetzt die eigentliche Nachricht. */
const gesendet = await ruf(`/workspace/api/chat/raeume/${raumId}/nachrichten`,
aDogi.keks, "POST", { text: "Kannst du das übernehmen, @VanVan? Danke!" });
ok(gesendet.code === 201, `die Nachricht geht raus (${gesendet.code})`);
const nachrichtId = json(gesendet).nachricht?.id;
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const zeilen = d.prepare("SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?")
.all(nachrichtId);
d.close();
ok(zeilen.length === 1 && zeilen[0].person_id === idVan,
`genau VanVan ist vermerkt (${zeilen.length} Eintrag/Einträge)`);
}
/* SELBST ERWÄHNEN LÖST NICHTS AUS. */
{
const s = await ruf(`/workspace/api/chat/raeume/${raumId}/nachrichten`,
aDogi.keks, "POST", { text: "@Filipe hat recht, sagt @Filipe" });
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const n = d.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ?")
.get(json(s).nachricht?.id).n;
d.close();
ok(n === 0, `wer sich selbst erwähnt, spricht niemanden an (${n} Einträge)`);
}
/* WER NICHT IM RAUM IST, KANN NICHT ANGESPROCHEN WERDEN -- auch nicht
über die Schnittstelle. Dafür ein vierter Mensch, der draußen bleibt. */
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const salt = randomBytes(16).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,1,?)").run("Draussen", "modi", "x", salt, 32768, jetzt);
const idAussen = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close();
const s = await ruf(`/workspace/api/chat/raeume/${raumId}/nachrichten`,
aDogi.keks, "POST", { text: "@Draussen bist du da?" });
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const n = d2.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ? AND person_id = ?")
.get(json(s).nachricht?.id, idAussen).n;
d2.close();
ok(n === 0, "wer nicht im Raum sitzt, wird auch über die Schnittstelle nicht angesprochen");
}
/* =====================================================================
3. MERKT ES DER RICHTIGE? — das Zeichen in der Gesprächsliste
===================================================================== */
melde("\n=== 3. Man merkt es auch ohne offenen Raum ===");
{
const beiVan = json(await ruf("/workspace/api/chat/raeume", aVan.keks))
.raeume?.find((r) => r.id === raumId);
const beiFunny = json(await ruf("/workspace/api/chat/raeume", aFunny.keks))
.raeume?.find((r) => r.id === raumId);
ok(beiVan?.erwaehnt === true, "VanVan sieht das Zeichen in ihrer Gesprächsliste");
/* DIE GEGENPROBE, die alles trägt: Funny war im selben Raum, hat
dieselben Nachrichten ungelesen -- und ist NICHT gemeint. Leuchtete
es bei ihr auch, wäre das Zeichen wertlos. */
ok(beiFunny?.erwaehnt === false,
"Gegenprobe: Funny war dabei, aber nicht gemeint — bei ihr leuchtet nichts");
ok((beiFunny?.ungelesen || 0) > 0,
` und sie hat sehr wohl Ungelesenes (${beiFunny?.ungelesen}) — es liegt nicht daran`);
}
/* GELESEN HEISST WEG. Sonst leuchtet es für immer. */
{
/* 99999 IST ABSICHTLICH ZU GROSS -- und genau daran ist eine echte
Luecke aufgefallen (20.09.2026): Der Server nahm jede Zahl an.
Wer einmal "gelesen bis 999999" schickte, sah in diesem Gespraech
nie wieder einen Zaehler und nie wieder ein @-Zeichen; alles
Kuenftige galt als gelesen, bevor es geschrieben war. Niemand
haette den Zusammenhang je gefunden.
Der Wert bleibt deshalb stehen: Er ist ab jetzt die Gegenprobe. */
await ruf(`/workspace/api/chat/raeume/${raumId}/gelesen`, aVan.keks, "PUT", { bis: 99999 });
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const stand = d.prepare(
"SELECT gelesen_bis FROM chat_teilnehmer WHERE raum_id = ? AND person_id = ?")
.get(raumId, idVan).gelesen_bis;
const hoechste = d.prepare(
"SELECT COALESCE(MAX(id),0) AS n FROM chat_nachrichten WHERE raum_id = ?").get(raumId).n;
d.close();
ok(stand === hoechste,
`„gelesen bis 99999" wird auf die letzte echte Nachricht begrenzt (${stand} statt 99999, höchste ist ${hoechste})`);
}
const beiVan = json(await ruf("/workspace/api/chat/raeume", aVan.keks))
.raeume?.find((r) => r.id === raumId);
ok(beiVan?.erwaehnt === false, "nach dem Lesen ist das Zeichen weg");
}
/* =====================================================================
4. WAS AUF DEM HANDY STEHT
===================================================================== */
melde("\n=== 4. Die Meldung sagt, dass DU gemeint bist ===");
{
const { pushTextFuer } = await import("./workspace-chat.js");
const n = { von: "VanVan", text: "schaust du mal drauf?" };
const erw = pushTextFuer("erwaehnung", n, raumId);
const normal = pushTextFuer("neu", n, raumId);
ok(/hat dich erwähnt/.test(erw.titel), `-> ${erw.titel}`);
ok(erw.titel !== normal.titel,
` und das ist etwas anderes als: ${normal.titel}`);
ok(erw.text.includes("schaust du mal"),
" der Anfang der Nachricht steht darunter — man muss nicht öffnen");
ok(erw.ziel === normal.ziel && erw.ziel.includes(String(raumId)),
" und beide führen in dasselbe Gespräch");
/* Die Art muss in der Liste stehen, sonst lässt sie sich nicht
abschalten -- und eine Meldung, die man nicht abschalten kann,
schaltet irgendwann alle ab. */
const { ARTEN } = await import("./workspace-push.js");
const art = ARTEN.find((a) => a.schluessel === "chat_erwaehnung");
ok(!!art, 'chat_erwaehnung steht in den Benachrichtigungs-Einstellungen');
ok(art?.vorgabe === true, " und ist von sich aus an");
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
" getrennt von der gewöhnlichen Chatnachricht — zwei Schalter, zwei Entscheidungen");
}
/* =====================================================================
5. BEIDE RECHNUNGEN, NEBENEINANDER
===================================================================== */
melde("\n=== 5. Server und Browser rechnen gleich ===");
{
/* Der Browser hat eine eigene, kurze Fassung derselben Regeln --
er muss sofort hervorheben können, ohne zu fragen. Genau deshalb
wird sie hier gegen das Original gestellt: Zwei Rechnungen für
dieselbe Frage laufen sonst irgendwann auseinander, und dann hebt
die Seite jemanden hervor, den niemand benachrichtigt hat. */
/* ZEILENENDEN VEREINHEITLICHEN -- und das ist kein Schoenheitsfehler.
Der erste Anlauf suchte das Ende der Funktion mit einem Muster,
das auf einem Zeilenumbruch endete. Die Dateien dieses Hauses
stehen unter Windows aber mit CRLF auf der Platte; nach " }"
kommt also erst ein Wagenruecklauf -- und das Muster fand
NICHTS. Die Pruefung stuerzte ab, was hier Glueck war: Haette
sie mit dem leeren Ergebnis weitergerechnet, waere sie gruen
geworden und haette null Proben verglichen. */
const chatJs = readFileSync(join(HIER, "..", "workspace", "assets", "js", "chat.js"), "utf8")
.replace(/\r\n/g, "\n");
ok(/erwaehnungenFinden/.test(chatJs),
"der Browser hat eine Fassung derselben Auflösung");
/* Sie wird aus der Datei geholt und ausgeführt -- nicht nachgebaut.
Ein Nachbau würde prüfen, ob ICH zweimal dasselbe schreiben kann. */
const anfang = chatJs.indexOf("function erwaehnungenFinden");
ok(anfang > 0, " und sie steht als eigene Funktion da");
if (anfang > 0) {
/* Den Block bis zur schließenden Klammer auf Spaltenhöhe holen. */
const rest = chatJs.slice(anfang);
const ende = rest.indexOf("\n }\n");
/* DRITTER AUSGANG: Findet der Schnitt sein Ende nicht, wird das
GESAGT -- statt mit einem Stummel weiterzurechnen und dabei
null Proben zu vergleichen. */
ok(ende > 100, `das Ende der Funktion ist auffindbar (Zeichen ${ende})`);
const quelle = rest.slice(0, ende + 4);
// eslint-disable-next-line no-new-func
const browserFassung = new Function(`${quelle}; return erwaehnungenFinden;`)();
const leute = [
{ id: 1, name: "Dogfather" }, { id: 2, name: "VanVan" }, { id: 3, name: "Tili" },
{ id: 4, name: "Miss" },
];
const proben = [
"@VanVan kannst du?", "@vanvan klein", "Hey @Tili, und @Miss!",
"[email protected]", "@Tilikum", "@Missgeschick", "x@VanVan",
"(@Miss)", "@Tili @Tili", "nichts hier", "@VanVan-Team",
];
let gleich = 0;
for (const t of proben) {
const a = JSON.stringify(erwaehnungenFinden(t, leute));
const b = JSON.stringify(browserFassung(t, leute));
if (a === b) gleich++;
else console.log(` ungleich bei ${JSON.stringify(t)}:\n Server: ${a}\n Browser: ${b}`);
}
ok(gleich === proben.length && proben.length >= 10,
`beide kommen bei allen ${proben.length} Proben auf dasselbe Ergebnis`);
}
}
/* =====================================================================
6. AM ECHTEN BILDSCHIRM
Alles bisher hat bewiesen, dass der Richtige vermerkt wird. Das ist
die Hälfte. Die andere: Sieht man es? Und kommt man mit der Tastatur
dorthin, ohne mitten im Satz zur Maus zu greifen?
Es gibt einen Weg, auf dem alles Bisherige grün ist und die Funktion
trotzdem nicht benutzbar: wenn die Liste hinter der Schreibzeile
liegt, wenn Enter statt zu wählen die halbe Nachricht abschickt,
oder wenn der Name am Ende grau bleibt wie jedes andere Wort.
===================================================================== */
melde("\n=== 6. Am echten Bildschirm ===");
const BASIS = `http://127.0.0.1:${PORT}`;
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
try {
async function alsRolle(rolle, code, breite = 1280, wohin = `chat.html?raum=${raumId}`) {
const ctx = await browser.newContext({ viewport: { width: breite, height: 900 } });
const seite = await ctx.newPage();
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 25000 });
await seite.goto(`${BASIS}/workspace/${wohin}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1100);
return { ctx, seite };
}
const dogi = await alsRolle("admin", "CODE-DOGI-0001");
const s = dogi.seite;
ok(await s.locator("#text").isVisible(), "das Schreibfeld steht da");
/* ---- Die Liste erscheint beim Tippen --------------------------- */
const vorher = await s.locator(".chat-nachricht").count();
await s.click("#text");
await s.type("#text", "Hallo @", { delay: 40 });
await s.waitForTimeout(400);
const liste = s.locator("#erwaehn-liste");
ok(await liste.isVisible(), 'ein @ oeffnet die Liste');
const namen = await liste.locator(".erwaehn-liste__name").allInnerTexts();
ok(namen.length === 2, `sie zeigt die anderen zwei aus diesem Raum (${namen.join(", ")})`);
ok(!namen.includes("Filipe"),
" und NICHT mich selbst — ein Eintrag, der nichts bewirkt, gehört nicht hinein");
/* Liegt sie auch wirklich sichtbar, oder hinter der Schreibzeile? */
const lage = await s.evaluate(() => {
const l = document.getElementById("erwaehn-liste");
const f = document.getElementById("text");
const a = l.getBoundingClientRect(), b = f.getBoundingClientRect();
return { unten: a.bottom, oben: b.top, breit: a.width, hoch: a.height,
imBild: a.top >= 0 && a.left >= 0 && a.right <= innerWidth };
});
ok(lage.unten <= lage.oben + 12,
`sie steht ÜBER der Schreibzeile (${Math.round(lage.unten)} ≤ ${Math.round(lage.oben)})`);
ok(lage.imBild && lage.breit > 100 && lage.hoch > 40,
` und vollständig im Bild (${Math.round(lage.breit)}×${Math.round(lage.hoch)})`);
/* ---- Tippen filtert --------------------------------------------- */
await s.type("#text", "Van", { delay: 40 });
await s.waitForTimeout(350);
const gefiltert = await liste.locator(".erwaehn-liste__name").allInnerTexts();
ok(gefiltert.length === 1 && gefiltert[0] === "VanVan",
`Weitertippen grenzt ein (${gefiltert.join(", ") || "nichts"})`);
/* ---- Die Berührfläche ------------------------------------------- */
const hoch = await liste.locator(".erwaehn-liste__eintrag").first()
.evaluate((e) => e.getBoundingClientRect().height);
ok(hoch >= 44, `ein Eintrag ist ${Math.round(hoch)} px hoch — am Daumen treffbar`);
/* ---- Enter WÄHLT, es schickt nicht ab ---------------------------- */
await s.keyboard.press("Enter");
await s.waitForTimeout(400);
const nachEnter = await s.inputValue("#text");
ok(nachEnter === "Hallo @VanVan ",
`Enter setzt den Namen ein: ${JSON.stringify(nachEnter)}`);
ok(!(await liste.isVisible()), " und die Liste ist wieder zu");
/* DER ENTSCHEIDENDE PUNKT: Der halbe Satz darf NICHT rausgegangen
sein. Ohne diese Prüfung wäre der häufigste Handgriff überhaupt
eine versehentlich abgeschickte Nachricht. */
const raus = await s.locator(".chat-nachricht").count();
/* GEGEN DEN AUSGANGSWERT, NICHT GEGEN NULL. Im Raum stehen schon
die Nachrichten aus Abschnitt 2 -- der erste Anlauf verglich mit
0 und wurde rot, obwohl nichts abgeschickt worden war. Eine
Pruefung, die den Anfangszustand nicht kennt, misst ihn mit. */
ok(raus === vorher, `nichts wurde abgeschickt (${raus}, vorher ${vorher})`);
/* ---- Und jetzt wirklich abschicken ------------------------------- */
await s.type("#text", "schaust du mal?", { delay: 20 });
await s.keyboard.press("Enter");
await s.waitForTimeout(1400);
const blase = s.locator(".chat-nachricht").last();
ok(await blase.isVisible(), "die Nachricht steht im Verlauf");
const marke = blase.locator(".chat-erwaehnung");
ok(await marke.count() === 1, "der Name ist im Satz hervorgehoben");
ok((await marke.innerText()).trim() === "@VanVan",
` und zwar genau: ${(await marke.innerText()).trim()}`);
/* GEGENPROBE: Beim Absender ist es NICHT der eigene Name -- die
stärkere Hervorhebung gehört nur dem, der gemeint ist. */
ok(await marke.getAttribute("data-ich") === null,
"Gegenprobe: beim Absender ist es nicht die eigene Marke");
/* ---- Und bei der, die gemeint ist -------------------------------- */
const van = await alsRolle("scout", "CODE-SCOU-0001");
const vs = van.seite;
await vs.waitForTimeout(900);
const vMarke = vs.locator(".chat-nachricht").last().locator(".chat-erwaehnung");
ok(await vMarke.count() === 1, "VanVan sieht den Namen ebenfalls hervorgehoben");
ok(await vMarke.getAttribute("data-ich") === "ja",
" und bei ihr deutlicher — sie ist gemeint");
/* NICHT NUR FARBE: Im Kontrastmodus fällt jede Tönung weg. Geprüft
wird, dass auch etwas anderes anders ist. */
const anders = await vMarke.evaluate((e) => {
const m = getComputedStyle(e);
const nachbar = e.parentElement;
const n = getComputedStyle(nachbar);
return { gewicht: m.fontWeight, gewichtNachbar: n.fontWeight,
schatten: m.boxShadow !== "none", grund: m.backgroundColor };
});
ok(Number(anders.gewicht) > Number(anders.gewichtNachbar) || anders.schatten,
` erkennbar nicht nur an der Farbe (Gewicht ${anders.gewicht} statt ${anders.gewichtNachbar}, Rahmen: ${anders.schatten})`);
/* ---- Das Zeichen in der Gesprächsliste ---------------------------
ERST ZURÜCK ZUR LISTE, DANN EINE FRISCHE ERWÄHNUNG. Der erste
Anlauf prüfte es direkt nach dem Lesen und wurde rot -- zu Recht:
VanVan hatte den Raum offen, also hat sie es gesehen, und ein
Zeichen für etwas Gelesenes wäre falsch. Die Prüfung mass die
eigene Reihenfolge, nicht das Verhalten.
Jetzt der Fall, um den es wirklich geht: Sie ist woanders, und es
kommt etwas für sie herein. */
await vs.goto(`${BASIS}/workspace/chat.html`, { waitUntil: "networkidle" });
await vs.waitForTimeout(900);
const vorSchon = await vs.locator(".chat-raum__erwaehnt").count();
ok(vorSchon === 0,
`solange nichts Neues da ist, leuchtet auch nichts (${vorSchon})`);
await s.click("#text");
await s.type("#text", "@VanVan noch eine Sache", { delay: 15 });
await s.keyboard.press("Enter");
await s.waitForTimeout(1500);
await vs.reload({ waitUntil: "networkidle" });
await vs.waitForTimeout(1200);
const zeichen = vs.locator(".chat-raum__erwaehnt");
ok(await zeichen.count() === 1, `in ihrer Gesprächsliste steht das @-Zeichen (${await zeichen.count()})`);
ok((await zeichen.first().getAttribute("aria-label") || "").includes("erwähnt"),
" mit einem Satz für Vorleseprogramme statt eines nackten Zeichens");
/* GEGENPROBE BEI FUNNY: Sie sitzt im selben Raum und hat dieselben
Nachrichten ungelesen -- und ist nicht gemeint. Leuchtete es bei
ihr auch, wäre das Zeichen wertlos. Am Bildschirm, nicht nur in
der Antwort des Servers. */
const funny = await alsRolle("scout", "CODE-SCOU-0002", 1280, "chat.html");
await funny.seite.waitForTimeout(1200);
const beiFunny = await funny.seite.locator(".chat-raum__erwaehnt").count();
const zahlFunny = await funny.seite.locator(".chat-raum__zahl").count();
ok(beiFunny === 0, `Gegenprobe: bei Funny leuchtet nichts (${beiFunny})`);
ok(zahlFunny >= 1,
` obwohl sie Ungelesenes hat (${zahlFunny} Gespräch(e) mit Zahl) — es liegt nicht daran`);
await funny.ctx.close();
/* ---- Die Gegenprobe der Liste: ein @ ohne Treffer -------------- */
await s.click("#text");
await s.type("#text", "@Zzz", { delay: 30 });
await s.waitForTimeout(350);
ok(!(await liste.isVisible()),
"Gegenprobe: passt niemand, bleibt die Liste zu");
/* Und Enter schickt dann wieder ab, wie immer. */
await s.keyboard.press("Enter");
await s.waitForTimeout(1200);
const jetztRaus = await s.locator(".chat-nachricht").count();
ok(jetztRaus > vorher + 1, ` und Enter schickt wieder ab (${jetztRaus}, vorher ${vorher})`);
await dogi.ctx.close();
await van.ctx.close();
} finally {
await browser.close();
}
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);
+142 -10
View File
@@ -47,6 +47,7 @@ import {
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE,
} from "./workspace.js";
import { erwaehnteIds } from "./chat-erwaehnung.js";
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
@@ -505,6 +506,32 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
WHERE raum_id = ? AND id > ? AND id > ? AND person_id <> ?`)
.get(r.id, r.gelesen_bis, r.geloescht_bis, req.person.id).n;
/* STEHT IN DEN UNGELESENEN EIN "@" AN MICH? (20.09.2026)
Eine Zahl sagt "hier ist etwas". Sie sagt nicht, ob es an
DICH war. Wer morgens vier Raeume mit Zahlen sieht, macht
den lautesten zuerst auf -- und genau dort steht selten das,
was auf ihn wartet. Das Zeichen daneben dreht die Reihenfolge
um.
DIESELBEN ZWEI GRENZEN wie bei der Zahl darueber: was schon
gelesen ist, und was weggeraeumt wurde. Ohne die zweite
leuchtete ein Gespraech wieder auf, das jemand bewusst
beiseitegelegt hat.
UND `weg_am IS NULL`: Eine zurueckgenommene Nachricht spricht
niemanden mehr an. Der Eintrag bleibt stehen (er gehoert zur
Nachricht), zaehlt hier aber nicht mehr mit -- gerechnet wird
beim Lesen, nicht beim Zuruecknehmen, damit es keine zweite
Stelle gibt, die man vergessen kann. */
const erwaehnt = !!db().prepare(`
SELECT 1 FROM chat_erwaehnungen e
JOIN chat_nachrichten n ON n.id = e.nachricht_id
WHERE e.person_id = ? AND n.raum_id = ?
AND n.id > ? AND n.id > ? AND n.weg_am IS NULL
LIMIT 1`)
.get(req.person.id, r.id, r.gelesen_bis, r.geloescht_bis);
return {
id: r.id,
art: r.art,
@@ -531,6 +558,7 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
von: letzte.von, erstellt: letzte.erstellt,
} : null,
ungelesen,
erwaehnt,
};
});
/* WELCHE KANAELE ES NOCH NICHT GIBT -- und nur fuer die, die sie
@@ -1042,11 +1070,45 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
anhang: null,
};
/* ==== WER IST MIT "@" GEMEINT? (20.09.2026) =====================
Aufgeloest wird HIER, gegen die Teilnehmer DIESES Raums -- nicht
im Browser. Der Browser schickt nur Text. Wer daraus eine
Person wird, entscheidet der Server, und damit gilt:
* Ein von Hand getipptes "@Anna" wirkt genauso wie eines aus
der Auswahlliste. Wer schnell schreibt, soll nicht bestraft
werden.
* Niemand kann jemanden ansprechen, der gar nicht im Raum
sitzt -- auch nicht, indem er die Schnittstelle direkt
anspricht.
Die Regeln stehen in chat-erwaehnung.js, samt Begruendung je
Regel. Sie sind bewusst ohne Abhaengigkeiten, damit die
Pruefung sie lesen kann, ohne einen Server hochzufahren. */
const alleLeute = teilnehmerVon(raumId);
const erwaehnt = erwaehnteIds(text, alleLeute)
/* SICH SELBST ANSPRECHEN LOEST NICHTS AUS. Man schreibt den
eigenen Namen durchaus ("@Anna hat recht, ich auch") -- eine
Meldung an sich selbst waere Unsinn. */
.filter((pid) => pid !== req.person.id);
if (erwaehnt.length) {
const einf = d.prepare(
"INSERT OR IGNORE INTO chat_erwaehnungen (nachricht_id, person_id) VALUES (?,?)");
for (const pid of erwaehnt) einf.run(id, pid);
}
/* Sofort an alle, die gerade zusehen -- und per Benachrichtigung
an die, die es nicht tun. Beides läuft über chatEreignis(),
damit die Entscheidung "wer bekommt was" an einer Stelle steht. */
const empfaenger = teilnehmerVon(raumId).filter((t) => t.id !== req.person.id);
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id));
damit die Entscheidung "wer bekommt was" an einer Stelle steht.
Auch die Erwaehnung: Sie ist keine ZWEITE Meldung, sondern eine
ANDERE -- wer angesprochen wurde, bekommt statt "Nachricht von
Anna" ein "Anna hat dich erwaehnt". Zwei Meldungen fuer eine
Nachricht waeren der schnellste Weg, dass jemand beide
abschaltet. */
const empfaenger = alleLeute.filter((t) => t.id !== req.person.id);
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id), "neu",
new Set(erwaehnt));
res.status(201).json({ nachricht });
} catch (fehler) {
@@ -1696,12 +1758,42 @@ chatRouter.put("/workspace/api/chat/raeume/:id/gelesen", gleicheHerkunft,
return res.status(400).json({ fehler: "ungueltig" });
}
if (!istDrin(raumId, req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
/* ==== NICHT WEITER ALS BIS ZUR LETZTEN NACHRICHT (20.09.2026) ====
Hier stand nur `Number.isInteger(bis)`. Damit liess sich der
Stand auf JEDE Zahl setzen -- auch auf eine, die weit hinter
allem liegt, was es gibt.
Die Folge ist still und dauerhaft: Wer einmal "gelesen bis
999999" schickt, hat in diesem Gespraech nie wieder einen
Zaehler und nie wieder ein @-Zeichen. Alles Kuenftige ist
schon gelesen, bevor es geschrieben wurde. Niemand wuerde den
Zusammenhang je finden -- man merkt nur, dass "die
Benachrichtigungen nicht gehen".
Gefunden beim Bauen der Erwaehnungen: Die Pruefung setzte zum
Aufraeumen 99999 und wunderte sich danach, warum das frische
"@" nicht leuchtete. Der Fehler lag nicht in der Erwaehnung.
Der Browser schickt heute immer die Nummer der letzten
gesehenen Nachricht -- er ist also nicht der Grund. Aber eine
Grenze, die nur davon lebt, dass der Aufrufer sich benimmt,
ist keine.
ABGELEITET STATT GERATEN: Weiter als bis zur juengsten
Nachricht dieses Raums kann niemand gelesen haben. */
const neueste = db().prepare(
"SELECT COALESCE(MAX(id), 0) AS n FROM chat_nachrichten WHERE raum_id = ?")
.get(raumId).n;
const stand = Math.max(0, Math.min(bis, neueste));
/* NUR VORWÄRTS. Ein zu spät eintreffender Aufruf einer älteren
Ansicht darf den Stand nicht zurückdrehen -- sonst stünde eine
gelesene Nachricht wieder als ungelesen da. */
db().prepare(`UPDATE chat_teilnehmer SET gelesen_bis = ?
WHERE raum_id = ? AND person_id = ? AND gelesen_bis < ?`)
.run(bis, raumId, req.person.id, bis);
.run(stand, raumId, req.person.id, stand);
res.json({ ok: true });
} catch (fehler) {
console.error("[chat] Gelesen:", fehler?.message);
@@ -1863,7 +1955,15 @@ chatRouter.delete("/workspace/api/chat/nachrichten/:id", gleicheHerkunft, (req,
* Eine Person kann an zwei Geräten sitzen. */
const zuschauer = new Map();
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu") {
/**
* @param erwaehnte Wer in DIESER Nachricht mit "@" angesprochen wurde.
* Diese Menge bekommt eine ANDERE Meldung, nicht eine zweite
* (20.09.2026). Sie steht hier und nicht an der Aufrufstelle,
* weil der Kommentar unten es verlangt: Die Entscheidung "wer
* bekommt was" gehoert an EINE Stelle.
*/
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
erwaehnte = new Set()) {
/* 1. An alle offenen Verbindungen der Beteiligten. */
const leute = teilnehmerVon(raumId).map((t) => t.id);
for (const personId of leute) {
@@ -1898,7 +1998,12 @@ function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu") {
sehen und behandelte deshalb JEDES Ereignis als Chatnachricht.
Bei einem Anruf kam dabei woertlich „Nachricht von [object
Object]" heraus. */
void meldeNeueNachricht(personId, raumId, nachricht, art);
/* ANGESPROCHEN ODER NUR DABEI? Der Unterschied entscheidet ueber
Wortlaut UND Art -- und die Art kann jeder einzeln abschalten.
Wer den Chat stumm stellt, weil dort viel geredet wird, will
trotzdem wissen, wenn ihn jemand direkt anspricht. */
void meldeNeueNachricht(personId, raumId, nachricht,
art === "neu" && erwaehnte.has(personId) ? "erwaehnung" : art);
}
}
@@ -1946,9 +2051,30 @@ export function pushTextFuer(art, nachricht, raumId) {
};
}
const wer = typeof nachricht.von === "string"
? nachricht.von : (nachricht.von?.name || "jemandem");
/* ---- ANGESPROCHEN IST ETWAS ANDERES ALS DABEI (20.09.2026) ----
"Nachricht von Anna" sagt nicht, dass sie DICH meint. In einem
Raum mit acht Leuten ist das der Unterschied zwischen "kann
warten" und "gleich nachsehen" -- und genau diesen Unterschied
soll das Markieren mit "@" machen. Steht er nicht in der Meldung,
ist das Markieren nur Farbe.
Der Anfang der Nachricht bleibt darunter stehen, wie sonst auch:
Wer auf dem Sperrbildschirm liest, soll nicht oeffnen muessen, um
zu erfahren, ob es wichtig war. */
if (art === "erwaehnung") {
return {
titel: `${wer} hat dich erwähnt`,
text: String(nachricht.text || "").slice(0, 120),
ziel,
};
}
return {
titel: `Nachricht von ${typeof nachricht.von === "string"
? nachricht.von : (nachricht.von?.name || "jemandem")}`,
titel: `Nachricht von ${wer}`,
/* Der Anfang der Nachricht steht in der Meldung -- eine
Benachrichtigung, die nur "Neue Nachricht" sagt, zwingt zum
Öffnen, um zu erfahren, ob es wichtig war. */
@@ -1987,8 +2113,14 @@ async function meldeNeueNachricht(personId, raumId, nachricht, art = "neu") {
wirklich klingelt. */
/* ALS EIGENE ART, wenn es ein Anruf ist. Sie umgeht die Ruhezeit
und laesst sich getrennt abschalten -- siehe workspace-push.js. */
await benachrichtige(personId, art === "anruf" ? "anruf" : "chat_nachricht",
pushTextFuer(art, nachricht, raumId));
/* DREI ARTEN, DREI SCHALTER. Jede laesst sich einzeln abschalten
(siehe ARTEN in workspace-push.js) -- und genau darum geht es:
Wer den lauten Chat stumm stellt, verliert damit weder Anrufe
noch die Faelle, in denen ihn jemand direkt anspricht. */
const pushArt = art === "anruf" ? "anruf"
: art === "erwaehnung" ? "chat_erwaehnung"
: "chat_nachricht";
await benachrichtige(personId, pushArt, pushTextFuer(art, nachricht, raumId));
} catch (fehler) {
console.error("[chat] Benachrichtigung:", fehler?.message);
}
+19
View File
@@ -87,6 +87,25 @@ export const ARTEN = [
die am haeufigsten kommt, und wer die Liste ueberfliegt, sucht
zuerst sie. */
{ schluessel: "chat_nachricht", name: "Neue Nachricht im Chat", vorgabe: true },
/* ---- ANGESPROCHEN WERDEN IST EINE EIGENE ART (20.09.2026) ------
Filipe: "ich will das man die leute mit @ markieren kann im chat."
Sie steht direkt hinter "Neue Nachricht", weil sie daneben
gehoert -- und sie ist trotzdem getrennt, aus demselben Grund,
aus dem Anrufe getrennt sind: Es sind zwei verschiedene
Entscheidungen.
In einem Raum mit acht Leuten ist "jemand hat geschrieben" oft
Hintergrund. "Jemand meint DICH" ist es nie. Wer das Erste stumm
stellt, will das Zweite meist behalten -- und muesste sonst
beides zusammen abschalten oder beides zusammen ertragen.
`vorgabe: true`, wie alles, was auf ein Ereignis antwortet. Und
KEINE Ausnahme von der Ruhezeit: Ein Anruf wartet auf eine
Antwort, ein "@Anna" nicht. Wer um halb vier angesprochen wird,
liest es um sieben -- das reicht. */
{ schluessel: "chat_erwaehnung", name: "Jemand erwähnt dich im Chat (@)", vorgabe: true },
/* ---- ANRUFE SIND EINE EIGENE ART (18.09.2026) ------------------
Filipe: „sie bekommt nur eine benarichtigung das eine neue
+43
View File
@@ -3624,6 +3624,49 @@ export function db() {
CREATE INDEX IF NOT EXISTS idx_chat_nachrichten_raum
ON chat_nachrichten (raum_id, id);
/* ERWAEHNUNGEN -- "@Name" IM CHAT (20.09.2026).
Filipe: "ich will das man die leute mit @ markieren kann im
chat."
WARUM EINE EIGENE TABELLE UND NICHT NUR FARBIGER TEXT:
Markieren ist kein Schmuck, sondern eine Ansprache. Wer
gemeint ist, soll es MERKEN -- auch wenn er den Raum gerade
nicht offen hat und auch dann noch, wenn er ihn Stunden
spaeter aufmacht. Dafuer muss irgendwo stehen, wer gemeint
war.
WARUM NICHT AUS DEM TEXT NACHRECHNEN: Man koennte beim Lesen
jedes Mal "@" suchen. Dann muesste man aber bei jedem Aufbau
der Gespraechsliste den Text JEDER ungelesenen Nachricht
durchgehen -- bei einem lebhaften Raum sind das hunderte. Hier
steht das Ergebnis einmal und wird nie wieder gerechnet.
WER GEMEINT IST, ENTSCHEIDET DER SERVER -- nicht der Browser.
Der Browser schickt nur Text; wer daraus eine Person wird,
loest erwaehnungenFinden() gegen die TEILNEHMER DES RAUMS
auf. So kann niemand jemanden ansprechen, der gar nicht dabei
ist, und ein von Hand getipptes "@Anna" wirkt genauso wie
eines aus der Auswahlliste.
ON DELETE CASCADE an beiden Enden, wie bei den Reaktionen:
Verschwindet die Nachricht oder der Mensch, geht der Eintrag
mit. Eine Erwaehnung ohne Nachricht waere ein Hinweis, der
nirgendwohin fuehrt.
Der zusammengesetzte Primaerschluessel sagt "jede Person hoechstens
einmal je Nachricht": Wer dreimal "@Anna" in einen Satz
schreibt, spricht sie trotzdem nur einmal an. */
CREATE TABLE IF NOT EXISTS chat_erwaehnungen (
nachricht_id INTEGER NOT NULL REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
PRIMARY KEY (nachricht_id, person_id)
);
/* Die Frage, die im Betrieb gestellt wird, lautet "bin ICH
irgendwo erwaehnt?" -- deshalb liegt person_id vorn. */
CREATE INDEX IF NOT EXISTS idx_chat_erwaehnungen_person
ON chat_erwaehnungen (person_id, nachricht_id);
/* REAKTIONEN (14.09.2026).
Filipe: "ich will dass man auf die nachrichten auch reagieren
+4 -4
View File
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
<!-- Ohne diese drei nimmt ein iPhone einen Bildschirmausschnitt als
Symbol, Android zeichnet die Leiste weiss, und die Seite oeffnet
ausserhalb der App -- auf einer Seite, die genau erklaeren soll,
WARUM das Telefon nicht klingelt, ist das besonders unpassend. -->
<link rel="manifest" href="/workspace/crew.webmanifest?v=202609191018" />
<meta name="theme-color" content="#06090f" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<style>
/* Bewusst in der Seite und nicht in einer eigenen Datei: Diese Seite
soll auch dann noch etwas anzeigen, wenn genau das schiefgeht,
@@ -125,6 +125,6 @@
</main>
␍␍
<script src="assets/js/anruf-probe.js?v=202609201727" defer></script>
<script src="assets/js/anruf-probe.js?v=202609201750" defer></script>
</body>
</html>
+4 -4
View File
@@ -22,25 +22,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609201727",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609201750",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609201727",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609201750",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609201727",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609201750",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609201727",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609201750",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+121
View File
@@ -1992,3 +1992,124 @@
border: 2px solid CanvasText;
}
}
/* =====================================================================
"@" IM CHAT (20.09.2026)
Filipe: "ich will das man die leute mit @ markieren kann im chat."
===================================================================== */
/* ---- DER NAME IM SATZ ----------------------------------------------
GEDECKT, NICHT GRELL. Ein markierter Name steht mitten in einem
Absatz, den man liest -- er soll auffallen, ohne den Satz zu
zerreissen. Deshalb eine leichte Unterlage und etwas mehr Gewicht
statt einer Signalfarbe: Wer in einem langen Gespraech blaettert,
sieht sonst nur noch die Markierungen. */
.chat-erwaehnung {
color: var(--akzent);
font-weight: 640;
padding: 0 3px;
margin: 0 -1px;
border-radius: 5px;
background: color-mix(in srgb, var(--akzent) 13%, transparent);
}
/* BIN ICH GEMEINT, WIRD ES DEUTLICHER. Das ist der ganze Zweck: In
einem Raum mit acht Leuten soll der eigene Name aus dem Absatz
herausspringen. Der Rahmen kommt dazu, damit es nicht NUR die Farbe
ist -- wer Farben schlecht unterscheidet, sieht trotzdem eine
Umrandung. */
.chat-erwaehnung[data-ich="ja"] {
background: color-mix(in srgb, var(--akzent) 26%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--akzent) 45%, transparent);
color: var(--text);
}
/* ---- DAS ZEICHEN IN DER GESPRÄCHSLISTE ------------------------------ */
.chat-raum__erwaehnt {
flex: none;
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px;
border-radius: 50%;
font-size: .82rem; font-weight: 800; line-height: 1;
color: var(--akzent);
background: color-mix(in srgb, var(--akzent) 18%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--akzent) 40%, transparent);
}
/* ---- DIE AUSWAHL BEIM TIPPEN ---------------------------------------
ÜBER der Schreibzeile, linksbündig: Das "@" wird links getippt, und
eine Liste, die woanders aufgeht als dort, wo man schreibt, sucht man
beim ersten Mal.
Dieselbe Verankerung wie bei der Emoji-Tafel (`bottom: 100%`), damit
beide sich gleich verhalten -- nur die Seite ist eine andere. */
.erwaehn-liste {
position: absolute;
left: 14px; right: auto; bottom: calc(100% - 6px);
z-index: 6;
min-width: min(18rem, calc(100vw - 32px));
max-width: calc(100vw - 32px);
/* MEHR ALS SECHS EINTRÄGE WERDEN GEROLLT statt die halbe Seite zu
verdecken. Acht ist die Obergrenze im Skript; sechs passen ohne
Rollen auf ein Handy. */
max-height: 17rem; overflow-y: auto;
display: flex; flex-direction: column; gap: 2px;
padding: 6px;
border-radius: 14px;
background: rgba(8, 12, 20, .97);
border: 1px solid rgba(150, 186, 220, .2);
box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .9);
}
.erwaehn-liste__eintrag {
appearance: none; border: 0; background: none;
display: flex; align-items: center; gap: 10px;
/* 44 px, wie jedes Berührziel in diesem Haus. Hier besonders: Wer
danebentippt, spricht den Falschen an -- und merkt es erst, wenn
die Nachricht schon draussen ist. */
width: 100%; min-height: 44px;
padding: 6px 10px;
border-radius: var(--radius-klein);
color: var(--text);
font: inherit; font-size: .92rem;
text-align: left; cursor: pointer;
transition: background var(--tempo);
}
.erwaehn-liste__eintrag:hover { background: rgba(255, 255, 255, .08); }
/* VORGEMERKT heisst: Enter nimmt diesen. Das muss man sehen, sonst
waehlt man blind. */
.erwaehn-liste__eintrag[data-wahl="ja"] {
background: color-mix(in srgb, var(--akzent) 20%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--akzent) 42%, transparent);
}
.erwaehn-liste__eintrag:focus-visible { outline: 2px solid var(--akzent); outline-offset: -2px; }
.erwaehn-liste__zeichen {
flex: none;
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 50%;
font-size: .72rem; font-weight: 700; line-height: 1;
background: rgba(255, 255, 255, .08);
color: var(--text-leise);
}
.erwaehn-liste__name { flex: 1 1 auto; font-weight: 600; }
.erwaehn-liste__rolle {
flex: none;
font-size: .72rem; color: var(--text-still);
text-transform: uppercase; letter-spacing: .06em;
}
/* ---- KONTRASTMODUS --------------------------------------------------
Dort gelten die Farben des Systems, und `color-mix` fällt weg: Eine
Markierung, die nur aus einer Tönung besteht, verschwindet komplett.
Deshalb hier ein echter Rahmen -- er überlebt jeden Farbmodus. */
@media (forced-colors: active) {
.chat-erwaehnung,
.chat-erwaehnung[data-ich="ja"] { border: 1px solid CanvasText; }
.chat-raum__erwaehnt { border: 1px solid CanvasText; }
.erwaehn-liste__eintrag[data-wahl="ja"] { border: 2px solid Highlight; }
}
+353 -18
View File
@@ -465,6 +465,23 @@
mitte.append(vorschau);
knopf.append(mitte);
/* WURDE ICH HIER ANGESPROCHEN? (20.09.2026)
Eine Zahl sagt "hier ist etwas". Sie sagt nicht, ob es an DICH
war. Wer morgens vier Gespraeche mit Zahlen sieht, macht das
lauteste zuerst auf -- und dort steht selten das, was auf ihn
wartet.
Das Zeichen steht VOR der Zahl, weil es die wichtigere Auskunft
ist, und es traegt einen eigenen Satz fuer Vorleseprogramme:
Ein "@" allein wird je nach Programm als "at", "Klammeraffe"
oder gar nicht vorgelesen. */
if (r.erwaehnt) {
const marke = el('span', 'chat-raum__erwaehnt', '@');
marke.title = 'Du wurdest hier erwähnt';
marke.setAttribute('aria-label', 'Du wurdest hier erwähnt');
knopf.append(marke);
}
if (r.ungelesen) {
const zahl = el('span', 'chat-raum__zahl', String(r.ungelesen));
zahl.setAttribute('aria-label', `${r.ungelesen} ungelesen`);
@@ -952,7 +969,22 @@
Die Seite sieht danach voellig normal aus. */
for (const alt of [...ziel.children]) if (alt.id !== 'verlauf-leer') alt.remove();
if (!nachrichten.length) {
ziel.append(el('p', 'chat__leer', 'Noch nichts geschrieben. Fang an.'));
/* WOHER WEISS JEMAND, DASS ES "@" GIBT? (20.09.2026)
Nirgends. Ein Zeichen, das man kennen muss, um es zu finden,
kennt beim ersten Mal niemand -- und im Chat gibt es keinen
Hilfeknopf, in den man es schreiben koennte.
Der Satz steht deshalb GENAU DORT, wo eine Gruppe anfaengt:
beim ersten Oeffnen, bevor etwas drinsteht. Er verschwindet mit
der ersten Nachricht und steht damit nie im Weg.
NUR BEI DREI ODER MEHR. In einem Zweiergespraech ist ohnehin
klar, wer gemeint ist -- dort waere der Hinweis Ballast. */
const inGruppe = (offen?.teilnehmer?.length || 0) > 2;
ziel.append(el('p', 'chat__leer', inGruppe
? 'Noch nichts geschrieben. Fang an — mit @ sprichst du jemanden direkt an.'
: 'Noch nichts geschrieben. Fang an.'));
return;
}
@@ -1073,7 +1105,7 @@
NUR WENN WIRKLICH TEXT DA IST: Eine Nachricht, die nur aus
einem Foto besteht, hätte sonst einen leeren Absatz darunter
-- und der nimmt Platz, den niemand erklären kann. */
if (n.text) blase.append(mitLinks(n.text));
if (n.text) blase.append(textAbsatz(n.text));
}
const fuss = el('div', 'chat-nachricht__fuss');
@@ -1335,23 +1367,289 @@
`rel="noopener noreferrer"` ist Pflicht: Ohne `noopener` kann die
geoeffnete Seite auf das Fenster zurueckgreifen, aus dem sie kam. */
const LINKMUSTER = /(https?:\/\/[^\s<>"']+|www\.[^\s<>"']+)/g;
function mitLinks(text) {
const p = el('p', 'chat-nachricht__text');
let letzte = 0;
for (const treffer of String(text).matchAll(LINKMUSTER)) {
const roh = treffer[0];
/* Satzzeichen am Ende gehoeren zum Satz, nicht zur Adresse. */
const sauber = roh.replace(/[.,;:!?)\]]+$/, '');
if (treffer.index > letzte) p.append(document.createTextNode(text.slice(letzte, treffer.index)));
const a = el('a', 'chat-nachricht__link', sauber);
a.href = sauber.startsWith('www.') ? 'https://' + sauber : sauber;
a.target = '_blank';
a.rel = 'noopener noreferrer';
p.append(a);
if (sauber.length < roh.length) p.append(document.createTextNode(roh.slice(sauber.length)));
letzte = treffer.index + roh.length;
/* =====================================================================
WER IST MIT "@" GEMEINT? -- die Fassung fuer den Browser
(20.09.2026)
DIESELBEN REGELN WIE IM SERVER. Das Original samt Begruendung je
Regel steht in server/chat-erwaehnung.js; hier steht eine zweite,
wortgleiche Fassung.
WARUM UEBERHAUPT ZWEIMAL: Der Browser muss beim Tippen und beim
Zeichnen sofort wissen, welcher Teil des Satzes ein Name ist --
ohne zu fragen. Eine Nachfrage je Nachricht waere eine Abfrage
mehr fuer etwas, das sich aus dem vorliegenden Text ergibt.
UND WARUM DAS GEFAEHRLICH IST: Zwei Rechnungen fuer dieselbe Frage
laufen irgendwann auseinander -- und dann hebt die Seite jemanden
hervor, den der Server nie benachrichtigt hat, oder umgekehrt.
Deshalb stellt `pruef-erwaehnung.mjs` beide Fassungen an denselben
Proben nebeneinander. Sie LIEST diese Funktion aus der Datei und
fuehrt sie aus, statt sie nachzubauen: Ein Nachbau wuerde pruefen,
ob ich zweimal dasselbe schreiben kann.
SIE STEHT BEWUSST OHNE AUSSENBEZUG DA -- auch das Zeichenmuster
wird hier drinnen gebildet. Sonst liesse sie sich nicht einzeln
herausloesen und ausfuehren. */
function erwaehnungenFinden(text, leute) {
const WORT = /[\p{L}\p{N}_]/u;
const wortBei = (t, i) => (i >= 0 && i < t.length && WORT.test(t[i]));
const satz = String(text || '');
if (!satz.includes('@')) return [];
const nachLaenge = (Array.isArray(leute) ? leute.slice() : [])
.filter((x) => x && x.id != null && String(x.name || '').trim())
.sort((a, b) => String(b.name).length - String(a.name).length);
if (!nachLaenge.length) return [];
const klein = satz.toLowerCase();
const treffer = [];
for (let i = klein.indexOf('@'); i !== -1; i = klein.indexOf('@', i + 1)) {
if (wortBei(satz, i - 1)) continue;
for (const person of nachLaenge) {
const name = String(person.name);
if (!klein.startsWith(name.toLowerCase(), i + 1)) continue;
if (wortBei(satz, i + 1 + name.length)) continue;
treffer.push({ id: person.id, name, von: i, bis: i + 1 + name.length });
break;
}
}
if (letzte < text.length) p.append(document.createTextNode(text.slice(letzte)));
return treffer;
}
/* =====================================================================
DIE AUSWAHL BEIM TIPPEN (20.09.2026)
Filipe: "ich will das man die leute mit @ markieren kann im chat."
WARUM EINE AUSWAHLLISTE UND NICHT NUR "@ TIPPEN GENUEGT":
Der Server erkennt auch ein von Hand getipptes "@VanVan" -- die
Liste ist also keine Pflicht, sondern eine Abkuerzung. Sie loest
aber drei Dinge, die man sonst selbst wissen muesste:
* WIE heisst die Person genau? "@Van" trifft nicht, "@vanvan"
schon. Ohne Liste probiert man.
* WER ist ueberhaupt in diesem Raum? Nur die koennen gemeint
sein -- das sieht man sonst nirgends beim Schreiben.
* Habe ich mich VERTIPPT? Ein "@Vanvna" faellt niemandem auf,
am wenigsten dem, der gemeint war.
WAS SIE BEWUSST NICHT TUT: Sie erzwingt nichts. Wer weitertippt,
ohne etwas zu waehlen, schreibt einfach weiter; die Liste
verschwindet, sobald nichts mehr passt. Eine Auswahl, aus der man
nicht herauskommt, ist eine Falle -- und im Chat schreibt man oft
ein "@" ohne jede Absicht ("um 10@Bahnhof").
DIE TASTATUR ZUERST: Pfeil hoch/runter, Enter oder Tab waehlt,
Escape schliesst. Am Rechner schreibt die Betreuung, und dort ist
der Griff zur Maus mitten im Satz der teuerste Handgriff
ueberhaupt.
===================================================================== */
/** Steht gerade eine Auswahl offen? Dann: ab welcher Stelle im Text
* (Position des "@") und welcher Eintrag ist vorgemerkt. */
let erwaehnAuswahl = null;
/** Die Stelle vor dem Schreibzeiger auf ein angefangenes "@..." hin
* ansehen.
*
* GIBT null ZURUECK, SOBALD ETWAS NICHT PASST -- und das ist der
* wichtigste Teil: Ein Leerzeichen beendet die Suche, ein Buchstabe
* vor dem "@" laesst sie gar nicht erst anfangen (sonst oeffnete
* sich die Liste mitten in jeder E-Mail-Adresse). Dieselbe Grenze
* wie beim Aufloesen, damit die Liste nichts anbietet, was der
* Server hinterher nicht erkennt. */
function angefangenesAt(feld) {
const bis = feld.selectionStart;
if (bis !== feld.selectionEnd) return null; // etwas ist markiert
const text = feld.value.slice(0, bis);
const at = text.lastIndexOf('@');
if (at === -1) return null;
const davor = at > 0 ? text[at - 1] : '';
if (davor && /[\p{L}\p{N}_]/u.test(davor)) return null;
const rumpf = text.slice(at + 1);
/* Ein Leerzeichen (oder ein Umbruch) beendet es. Namen in diesem
Haus sind einwortig; dieselbe Annahme steht im Server. */
if (/\s/.test(rumpf)) return null;
if (rumpf.length > 24) return null; // so heisst niemand
return { at, rumpf };
}
/** Wer passt gerade? -- die Teilnehmer dieses Raums, ohne mich. */
function erwaehnTreffer(rumpf) {
const klein = rumpf.toLowerCase();
return (offen?.teilnehmer || [])
/* SICH SELBST KANN MAN NICHT ANSPRECHEN -- der Server verwirft es
ohnehin. Ein Eintrag, der nichts bewirkt, gehoert nicht in die
Liste. */
.filter((t) => t.id !== ich?.id)
.filter((t) => String(t.name).toLowerCase().startsWith(klein))
.slice(0, 8);
}
function erwaehnSchliessen() {
erwaehnAuswahl = null;
const l = $('erwaehn-liste');
if (l) { l.hidden = true; l.textContent = ''; }
$('text')?.removeAttribute('aria-activedescendant');
}
/** Die Liste zeichnen -- oder schliessen, wenn nichts passt. */
function erwaehnPruefen() {
const feld = $('text');
const liste = $('erwaehn-liste');
if (!feld || !liste) return;
const stand = angefangenesAt(feld);
if (!stand) { erwaehnSchliessen(); return; }
const treffer = erwaehnTreffer(stand.rumpf);
if (!treffer.length) { erwaehnSchliessen(); return; }
/* Die Vormerkung bleibt auf DERSELBEN Person, solange sie noch
passt. Sonst springt die Auswahl bei jedem Buchstaben auf den
ersten Eintrag zurueck, und wer zweimal nach unten getippt hat,
verliert es beim naechsten Zeichen. */
const vorherId = erwaehnAuswahl?.treffer?.[erwaehnAuswahl.wahl]?.id;
const neueWahl = Math.max(0, treffer.findIndex((t) => t.id === vorherId));
erwaehnAuswahl = { at: stand.at, rumpf: stand.rumpf, treffer, wahl: neueWahl };
liste.textContent = '';
treffer.forEach((t, i) => {
const b = el('button', 'erwaehn-liste__eintrag');
b.type = 'button';
b.id = 'erwaehn-' + t.id;
b.setAttribute('role', 'option');
b.setAttribute('aria-selected', i === neueWahl ? 'true' : 'false');
if (i === neueWahl) b.dataset.wahl = 'ja';
const z = el('span', 'erwaehn-liste__zeichen', initialen(t.name));
z.setAttribute('aria-hidden', 'true');
z.dataset.rolle = t.rolle || '';
b.append(z, el('span', 'erwaehn-liste__name', t.name));
if (ROLLENKURZ[t.rolle]) b.append(el('span', 'erwaehn-liste__rolle', ROLLENKURZ[t.rolle]));
/* mousedown statt click: Ein Klick nimmt dem Textfeld zuerst den
Schreibzeiger, und danach weiss niemand mehr, wohin der Name
gehoert. `preventDefault` haelt ihn, wo er ist. */
b.addEventListener('mousedown', (e) => { e.preventDefault(); erwaehnWaehlen(i); });
liste.append(b);
});
liste.hidden = false;
feld.setAttribute('aria-activedescendant', 'erwaehn-' + treffer[neueWahl].id);
}
/** Den vorgemerkten Namen einsetzen. */
function erwaehnWaehlen(i) {
const a = erwaehnAuswahl;
const feld = $('text');
if (!a || !feld) return;
const person = a.treffer[i ?? a.wahl];
if (!person) return;
const bis = feld.selectionStart;
/* EIN LEERZEICHEN HINTERHER -- sonst klebt das naechste Wort am
Namen, und aus "@VanVankannst" wird gar keine Ansprache mehr.
Nur, wenn dort nicht schon eines steht. */
const rest = feld.value.slice(bis);
const luecke = rest.startsWith(' ') ? '' : ' ';
feld.value = feld.value.slice(0, a.at) + '@' + person.name + luecke + rest;
const neu = a.at + 1 + person.name.length + luecke.length;
feld.setSelectionRange(neu, neu);
erwaehnSchliessen();
feld.focus();
hoeheAnpassen();
entwurfMerken();
zaehlerZeigen();
}
/** Die Tasten, solange die Liste offen ist.
*
* GIBT true ZURUECK, wenn die Taste verbraucht ist -- der Aufrufer
* hoert dann auf. So bleibt Enter zum Abschicken erhalten, sobald
* keine Liste offen steht, und Escape verwirft weiterhin die
* Antwort. Ohne diese Rueckgabe muesste an zwei Stellen dieselbe
* Bedingung stehen. */
function erwaehnTaste(e) {
if (!erwaehnAuswahl) return false;
const n = erwaehnAuswahl.treffer.length;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const richtung = e.key === 'ArrowDown' ? 1 : -1;
erwaehnAuswahl.wahl = (erwaehnAuswahl.wahl + richtung + n) % n;
const knoepfe = $('erwaehn-liste').querySelectorAll('.erwaehn-liste__eintrag');
knoepfe.forEach((b, i) => {
b.setAttribute('aria-selected', i === erwaehnAuswahl.wahl ? 'true' : 'false');
if (i === erwaehnAuswahl.wahl) b.dataset.wahl = 'ja'; else delete b.dataset.wahl;
});
const gewaehlt = erwaehnAuswahl.treffer[erwaehnAuswahl.wahl];
$('text').setAttribute('aria-activedescendant', 'erwaehn-' + gewaehlt.id);
knoepfe[erwaehnAuswahl.wahl]?.scrollIntoView({ block: 'nearest' });
return true;
}
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); erwaehnWaehlen(); return true; }
if (e.key === 'Escape') { e.preventDefault(); erwaehnSchliessen(); return true; }
return false;
}
/* EIN ABSATZ, ZWEI SORTEN HERVORHEBUNG (erweitert 20.09.2026).
Hiess frueher `mitLinks` und konnte nur Adressen. Seit es das
Markieren mit "@" gibt, muessen ZWEI Sorten Stellen in denselben
Absatz -- und sie koennen sich ueberschneiden: In
"schau auf www.x.de/@Tili" steckt ein Name mitten in einer
Adresse.
DESHALB ERST SAMMELN, DANN SORTIEREN, DANN ZEICHNEN. Zwei
Schleifen nacheinander haetten den Text zweimal zerschnitten und
die zweite haette die Stuecke der ersten nicht gesehen. Bei einer
Ueberschneidung gewinnt, was frueher anfaengt; die Adresse ist
laenger und faengt frueher an, also bleibt sie ganz.
textContent, nie innerHTML -- das gilt unveraendert und ist der
Grund, warum hier Knoten gebaut und nicht Zeichenketten
zusammengeklebt werden. Eine Nachricht darf keine Auszeichnung
einschleusen, auch keinen eigenen Namen in spitzen Klammern. */
function textAbsatz(text) {
const p = el('p', 'chat-nachricht__text');
const roh = String(text);
const stellen = [];
for (const treffer of roh.matchAll(LINKMUSTER)) {
/* Satzzeichen am Ende gehoeren zum Satz, nicht zur Adresse. */
const sauber = treffer[0].replace(/[.,;:!?)\]]+$/, '');
stellen.push({ art: 'link', von: treffer.index, bis: treffer.index + sauber.length, wort: sauber });
}
/* Gegen die Teilnehmer DIESES Raums -- wie im Server. Steht kein
Raum offen (etwa beim Suchen), wird nichts hervorgehoben: lieber
grau als falsch. */
for (const t of erwaehnungenFinden(roh, offen?.teilnehmer || [])) {
stellen.push({ art: 'erwaehnung', von: t.von, bis: t.bis,
wort: roh.slice(t.von, t.bis), id: t.id });
}
stellen.sort((a, b) => a.von - b.von || b.bis - a.bis);
let letzte = 0;
for (const st of stellen) {
if (st.von < letzte) continue; // liegt in einer frueheren Stelle
if (st.von > letzte) p.append(document.createTextNode(roh.slice(letzte, st.von)));
if (st.art === 'link') {
const a = el('a', 'chat-nachricht__link', st.wort);
a.href = st.wort.startsWith('www.') ? 'https://' + st.wort : st.wort;
a.target = '_blank';
a.rel = 'noopener noreferrer';
p.append(a);
} else {
/* KEIN KNOPF, SONDERN EINE MARKE. Ein Klick darauf muesste
irgendwohin fuehren -- zum Steckbrief? in ein Zweiergespraech?
Beides waere eine Entscheidung, die niemand bestellt hat, und
ein Knopf, der nichts tut, ist schlimmer als keiner.
Hervorgehoben wird, damit man es SIEHT; mehr verspricht es
nicht. */
const m = el('span', 'chat-erwaehnung', st.wort);
/* BIN ICH GEMEINT? Dann deutlicher. Das ist der eigentliche
Zweck: In einem Raum mit acht Leuten soll der eigene Name
aus dem Absatz herausspringen. */
if (ich && st.id === ich.id) m.dataset.ich = 'ja';
p.append(m);
}
letzte = st.bis;
}
if (letzte < roh.length) p.append(document.createTextNode(roh.slice(letzte)));
return p;
}
@@ -1992,6 +2290,10 @@
zur Maus. */
$('antwort-weg')?.addEventListener('click', antwortLoeschen);
$('text')?.addEventListener('keydown', (e) => {
/* ERST DIE AUSWAHLLISTE, DANN DIE ANTWORT. Escape raeumt immer
das Oberste weg -- steht eine Liste offen, ist sie gemeint.
Sonst verloere man mit einem Tastendruck beides auf einmal. */
if (erwaehnTaste(e)) return;
if (e.key === 'Escape' && antwortAuf) { e.preventDefault(); antwortLoeschen(); }
});
@@ -2001,7 +2303,21 @@
hoeheAnpassen();
entwurfMerken();
zaehlerZeigen();
erwaehnPruefen();
});
/* AUCH BEIM BLOSSEN BEWEGEN DES SCHREIBZEIGERS. Wer mit der Maus
oder den Pfeiltasten hinter ein bereits getipptes "@Van" springt,
will dort weitermachen -- ohne diesen Weg bliebe die Liste zu, bis
er einen Buchstaben tippt. */
$('text').addEventListener('click', erwaehnPruefen);
$('text').addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight'
|| e.key === 'Home' || e.key === 'End') erwaehnPruefen();
});
/* Wer das Feld verlaesst, hat es sich anders ueberlegt. `mousedown`
auf einem Eintrag haelt den Schreibzeiger fest, deshalb schliesst
dieser Weg die Liste nicht versehentlich vor dem Klick. */
$('text').addEventListener('blur', erwaehnSchliessen);
/* ---------- Entwurf je Gespraech (09.09.2026) ----------------------------
Wer mitten im Satz das Gespraech wechselt, findet den Satz beim
@@ -2133,6 +2449,25 @@
NICHT -- dort ist Enter die Zeilenschaltung der Bildschirmtastatur,
und wer eine zweite Zeile will, käme sonst nie dazu. */
$('text').addEventListener('keydown', (e) => {
/* STEHT DIE AUSWAHL OFFEN, GEHOERT ENTER IHR. Ohne diese Zeile
waere jedes Waehlen mit der Tastatur ein versehentlich
abgeschickter halber Satz -- und zwar ausgerechnet fuer die, die
am Rechner schnell schreiben.
Der Ausgang steht VOR der Handy-Abfrage: Am Handy schickt Enter
ohnehin nicht ab, aber waehlen soll es auch dort.
GEFRAGT WIRD NACH `defaultPrevented`, NICHT NACH DER LISTE.
Der erste Anlauf fragte `if (erwaehnAuswahl && ...)` -- und lief
ins Leere: Der Handler weiter oben waehlt den Namen aus und
SCHLIESST dabei die Liste. Wenn dieser hier an die Reihe kommt,
ist `erwaehnAuswahl` schon null, die Bedingung falsch, und der
halbe Satz geht raus. Genau der Fehler, den die Funktion
verhindern soll.
`defaultPrevented` beantwortet die Frage, um die es wirklich
geht: Hat diese Taste ihre Arbeit schon getan? */
if (e.defaultPrevented) return;
const handy = window.matchMedia('(hover: none)').matches;
if (e.key === 'Enter' && !e.shiftKey && !handy) {
e.preventDefault();
+15 -15
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -291,14 +291,14 @@
</form>
</dialog>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/aufgaben.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/aufgaben.js?v=202609201750" defer></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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -168,13 +168,13 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/automation.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/automation.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -130,12 +130,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/befinden.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/befinden.js?v=202609201750" defer></script>
</body>
</html>
+19 -19
View File
@@ -12,24 +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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -256,16 +256,16 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/checkliste.js?v=202609201727" defer></script>
<script src="assets/js/ampel.js?v=202609201727" defer></script>
<script src="assets/js/schulung.js?v=202609201727" defer></script>
<script src="assets/js/bereich.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/checkliste.js?v=202609201750" defer></script>
<script src="assets/js/ampel.js?v=202609201750" defer></script>
<script src="assets/js/schulung.js?v=202609201750" defer></script>
<script src="assets/js/bereich.js?v=202609201750" defer></script>
</body>
</html>
+17 -17
View File
@@ -7,17 +7,17 @@
<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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -108,12 +108,12 @@
</main>
<!-- Der Fuß gehört zu jeder Seite - siehe pruef-css-klassen. -->
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/auskunft.js?v=202609201727" defer></script>
<script src="assets/js/bewerben.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/auskunft.js?v=202609201750" defer></script>
<script src="assets/js/bewerben.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,16 +7,16 @@
<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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -80,13 +80,13 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/auskunft.js?v=202609201727" defer></script>
<script src="assets/js/bewerbungen.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/auskunft.js?v=202609201750" defer></script>
<script src="assets/js/bewerbungen.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -89,14 +89,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/ki.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/calls.js?v=202609201727" defer></script>
<script src="assets/js/ki.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/calls.js?v=202609201750" defer></script>
</body>
</html>
+29 -18
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -278,7 +278,7 @@
</button>
<textarea id="text" rows="1" maxlength="4000"
placeholder="Nachricht schreiben …"
title="Enter schickt ab, Umschalt+Enter macht eine neue Zeile"></textarea>
title="Enter schickt ab, Umschalt+Enter macht eine neue Zeile, @ erwähnt jemanden"></textarea>
<!-- EMOJI-AUSWAHL (09.09.2026). Die Bildschirmtastatur am Handy
hat sie ohnehin -- am Rechner nicht, und dort schreibt die
Betreuung. Dreissig feste Zeichen statt einer nachgeladenen
@@ -288,6 +288,17 @@
aria-label="Emoji einfügen" title="Emoji einfügen">☺</button>
<div class="emoji-tafel" id="emoji-tafel" role="group"
aria-label="Emoji auswählen" hidden></div>
<!-- WER IST GEMEINT? (20.09.2026)
Erscheint beim Tippen von "@" und zeigt die Leute DIESES
Gesprächs. Sie steht hier im Formular und nicht am
Seitenende, weil sie sich über der Schreibzeile öffnet --
ein absolut gesetzter Kasten braucht seinen Bezugspunkt in
der Nähe, sonst wandert er bei jedem Umbruch woanders hin.
`role=listbox` und `aria-activedescendant` am Textfeld:
Ein Vorleseprogramm sagt dann beim Blättern den Namen an,
statt nur "Bearbeitungsfeld". -->
<div class="erwaehn-liste" id="erwaehn-liste" role="listbox"
aria-label="Person erwähnen" hidden></div>
<!-- Erscheint erst, wenn es eng wird -- siehe chat.js. -->
<span class="chat__zeichen" id="zeichen" aria-live="polite" hidden></span>
<button type="submit" class="chat__senden" id="senden" aria-label="Abschicken">
@@ -373,18 +384,18 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609201727"></script>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727"></script>
<script src="assets/js/glocke.js?v=202609201727"></script>
<script src="assets/js/bereiche.js?v=202609201750"></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750"></script>
<script src="assets/js/glocke.js?v=202609201750"></script>
<!-- ANRUF VOR CHAT (18.09.2026): anruf.js stellt `window.anrufStarten`
bereit, und chat.js baut die Knoepfe nur, wenn es das gibt.
Andersherum waere die Reihenfolge eine stille Abhaengigkeit --
die Knoepfe fehlten, und niemand wuesste warum. -->
<script src="assets/js/anruf.js?v=202609201727"></script>
<script src="assets/js/chat.js?v=202609201727"></script>
<script src="assets/js/anruf.js?v=202609201750"></script>
<script src="assets/js/chat.js?v=202609201750"></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/content.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/content.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -179,14 +179,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/checkliste.js?v=202609201727" defer></script>
<script src="assets/js/content.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/checkliste.js?v=202609201750" defer></script>
<script src="assets/js/content.js?v=202609201750" 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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609201750" />
</head>
<body class="gate gate--crew">
@@ -358,6 +358,6 @@
␍
<script src="assets/js/gate.js?v=202609201727" defer></script>
<script src="assets/js/gate.js?v=202609201750" defer></script>
</body>
</html>
+4 -4
View File
@@ -22,25 +22,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609201727",
"src": "/assets/img/app-symbole/crew-192.png?v=202609201750",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609201727",
"src": "/assets/img/app-symbole/crew-512.png?v=202609201750",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609201727",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609201750",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609201727",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609201750",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -108,14 +108,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/dateien.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/dateien.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -106,12 +106,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/entwicklung.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/entwicklung.js?v=202609201750" defer></script>
</body>
</html>
+13 -13
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -146,12 +146,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/hilfe.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/hilfe.js?v=202609201750" 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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
</head>
<body class="gate">
@@ -411,6 +411,6 @@
</div>
␍␍
<script src="assets/js/gate.js?v=202609201727" defer></script>
<script src="assets/js/gate.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -383,14 +383,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/kalender.js?v=202609201727" defer></script>
<script src="assets/js/kalender-abo.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/kalender.js?v=202609201750" defer></script>
<script src="assets/js/kalender-abo.js?v=202609201750" defer></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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -444,12 +444,12 @@
</form>
</dialog>
<script src="assets/js/meldung.js?v=202609201727"></script>
<script src="assets/js/nachfrage.js?v=202609201727"></script>
<script src="assets/js/wahl.js?v=202609201727"></script>
<script src="assets/js/bereiche.js?v=202609201727"></script>
<script src="assets/js/kopf.js?v=202609201727"></script>
<script src="assets/js/glocke.js?v=202609201727"></script>
<script src="assets/js/leistung.js?v=202609201727"></script>
<script src="assets/js/meldung.js?v=202609201750"></script>
<script src="assets/js/nachfrage.js?v=202609201750"></script>
<script src="assets/js/wahl.js?v=202609201750"></script>
<script src="assets/js/bereiche.js?v=202609201750"></script>
<script src="assets/js/kopf.js?v=202609201750"></script>
<script src="assets/js/glocke.js?v=202609201750"></script>
<script src="assets/js/leistung.js?v=202609201750"></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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -453,13 +453,13 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/personen.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/personen.js?v=202609201750" defer></script>
</body>
</html>
+17 -17
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -214,15 +214,15 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/steckbrief.js?v=202609201727" defer></script>
<script src="assets/js/profil.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/steckbrief.js?v=202609201750" defer></script>
<script src="assets/js/profil.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -134,11 +134,11 @@
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/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/rechte.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/rechte.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/report.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/report.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -106,14 +106,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/ki.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/report.js?v=202609201727" defer></script>
<script src="assets/js/ki.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/report.js?v=202609201750" defer></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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -173,13 +173,13 @@
</div>
</div>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/scouting.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/scouting.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -678,13 +678,13 @@
</p>
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/start.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/start.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -85,14 +85,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/ki.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/startcheck.js?v=202609201727" defer></script>
<script src="assets/js/ki.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/startcheck.js?v=202609201750" defer></script>
</body>
</html>
+17 -17
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -150,15 +150,15 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/auskunft.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/steckbrief.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/auskunft.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/steckbrief.js?v=202609201750" defer></script>
</body>
</html>
+15 -15
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -111,12 +111,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/talente.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/talente.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/team.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/team.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -110,12 +110,12 @@
</main>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/team.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/team.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/team.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/team.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
@@ -84,11 +84,11 @@
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/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/teamlage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/teamlage.js?v=202609201750" defer></script>
</body>
</html>
+13 -13
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -112,12 +112,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/teilen.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/teilen.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -126,12 +126,12 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/treff-moderation.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/treff-moderation.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -221,11 +221,11 @@
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/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/auskunft.js?v=202609201727" defer></script>
<script src="assets/js/treff-regeln.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/auskunft.js?v=202609201750" defer></script>
<script src="assets/js/treff-regeln.js?v=202609201750" defer></script>
</body>
</html>
+15 -15
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -149,14 +149,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/uebersicht.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/uebersicht.js?v=202609201750" defer></script>
</body>
</html>
+12 -12
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -217,11 +217,11 @@
</main>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/unsere-seiten.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/unsere-seiten.js?v=202609201750" defer></script>
</body>
</html>
+14 -14
View File
@@ -7,15 +7,15 @@
<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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -144,11 +144,11 @@
</main>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>␍
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>␍
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/werdegang.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/werdegang.js?v=202609201750" defer></script>
</body>
</html>
+16 -16
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=202609201727" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201727" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609201727" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201727" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609201750" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609201750" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/start.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609201750" />
<link rel="stylesheet" href="assets/css/module.css?v=202609201750" />
<!-- 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=202609201727" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609201750" />
</head>
<body class="start">
@@ -190,14 +190,14 @@
</main>
<script src="assets/js/meldung.js?v=202609201727" defer></script>
<script src="assets/js/hochladen.js?v=202609201727" defer></script>
<script src="assets/js/nachfrage.js?v=202609201727" defer></script>
<script src="assets/js/meldung.js?v=202609201750" defer></script>
<script src="assets/js/hochladen.js?v=202609201750" defer></script>
<script src="assets/js/nachfrage.js?v=202609201750" defer></script>
<script src="assets/js/wahl.js?v=202609201727" defer></script>
<script src="assets/js/bereiche.js?v=202609201727" defer></script>
<script src="assets/js/kopf.js?v=202609201727" defer></script>
<script src="assets/js/glocke.js?v=202609201727" defer></script>
<script src="assets/js/wissen.js?v=202609201727" defer></script>
<script src="assets/js/wahl.js?v=202609201750" defer></script>
<script src="assets/js/bereiche.js?v=202609201750" defer></script>
<script src="assets/js/kopf.js?v=202609201750" defer></script>
<script src="assets/js/glocke.js?v=202609201750" defer></script>
<script src="assets/js/wissen.js?v=202609201750" defer></script>
</body>
</html>