Compare commits
5
Commits
8363b63374
..
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c44c42681 | ||
|
|
2c0553a6b2 | ||
|
|
5c3248ae8f | ||
|
|
1ab760be0c | ||
|
|
6eb4dd3c51 |
No files matched your search
@@ -10,11 +10,23 @@ export const HUSTLER_STUFE = stufeAus(buildWert(), 3)
|
||||
|
||||
Die `3` in dieser Zeile ist die Stufe der Produktion. Mehr ist zum Hochsetzen nicht zu ändern.
|
||||
|
||||
Es gibt drei Schwellen (alle in `src/lib/hustlerStufe.js`, mit je einer Funktion, die Bausteine und Tests fragen):
|
||||
|
||||
| Konstante | Wert | Funktion | Was sie freigibt |
|
||||
|---|---|---|---|
|
||||
| `KELLER_BEGEHBAR_AB` | 3 | `kellerBegehbar(stufe)` | der Keller als Bildschirm: Keller-Etage als Link, `?tab=botleads` (auch mit `&bot=`), Spur-Links in den Keller |
|
||||
| `STUFE_ANTWORTEN` | 4 | `antwortenOffen(stufe)` | die Antwort-Knöpfe im Pult (Dienst P4) |
|
||||
| `BOTLEADS_AB` | 6 | `botleadsAbrufbar(stufe)` | der Abruf `GET /api/workers/botleads` (Dienst P6): nur dann holt der Keller Daten |
|
||||
|
||||
**Keller begehbar** und **Bot-Leads abrufbar** sind getrennt: Ab Stufe 3 öffnet der Keller seine Szene (Arne, Regale, Register-Buch, Reiter, Suche), auch wenn der Dienst die Bot-Leads noch nicht liefert.
|
||||
Dann steht das Register leer da (siehe unten) und es geht keine Anfrage hinaus.
|
||||
|
||||
| Stufe | Dienst | Oberfläche |
|
||||
|---|---|---|
|
||||
| 1 bis 3 | P1 + P3 (state, events, settings, pause, trace, pult GET, claim, sent, discard, done, prospect) | Hochhaus, Detailansicht, Pult mit Übernehmen, Kopieren & öffnen, Als gesendet markieren, Verwerfen, Erledigt, E-Mail schreiben. **Kein Keller, keine Antwort-Knöpfe.** |
|
||||
| 4 und 5 | zusätzlich P4: `POST /pult/:id/paste`, `/reply`, `/approve-preview`, `/start-build` | zusätzlich im Pult-Brief: Antwort des Kunden einfügen, Als Mensch antworten / Jetzt senden, Freigeben & senden, Vorschau bauen. Kein Keller. |
|
||||
| 6 bis 9 | zusätzlich P6: `GET /botleads` | zusätzlich der Keller (Bot-Leads): Keller-Etage als Link, Bildschirm `?tab=botleads`, Deeplinks, Spur-Links. Entspricht dem vollen Stand von `75ed38e`. |
|
||||
| 1 und 2 | vor P3 (kein Produktionswert, nur für Tests des geschlossenen Kellers) | wie Stufe 3, nur der Keller ist zu: Keller-Etage zoomt, `?tab=botleads` fällt aufs Hochhaus, Spur-Links in den Keller sind Text. Keine Antwort-Knöpfe. |
|
||||
| 3 | P1 + P3 (zusätzlich pult GET, claim, sent, discard, done, prospect) | Hochhaus, Detailansicht, Pult mit Übernehmen, Kopieren & öffnen, Als gesendet markieren, Verwerfen, Erledigt, E-Mail schreiben. **Keller begehbar, aber leer** (kein Abruf der Bot-Leads). Keine Antwort-Knöpfe. Das ist die Produktion. |
|
||||
| 4 und 5 | zusätzlich P4: `POST /pult/:id/paste`, `/reply`, `/approve-preview`, `/start-build` | zusätzlich im Pult-Brief: Antwort des Kunden einfügen, Als Mensch antworten / Jetzt senden, Freigeben & senden, Vorschau bauen. Keller wie bei Stufe 3 (leer). |
|
||||
| 6 bis 9 | zusätzlich P6: `GET /botleads` | der Keller holt die Bot-Leads (Live-Daten): Register mit Zeilen, Suche, Raum-Reiter, Seiten, Akte. Entspricht dem vollen Stand von `75ed38e`. |
|
||||
|
||||
## Was unterhalb von Stufe 4 gesperrt ist (P4-Aktionen)
|
||||
|
||||
@@ -22,13 +34,27 @@ Die `3` in dieser Zeile ist die Stufe der Produktion. Mehr ist zum Hochsetzen ni
|
||||
Brief und Liste lesen nur diese Felder, deshalb verschwinden Knöpfe, Eingabefelder, Rückfragen und Hinweise zusammen; die Aktionsgruppe schließt sich ohne Lücke.
|
||||
- `PultList` reicht `stufe` (Standard `HUSTLER_STUFE`) an `rowModel` durch. P3-Aktionen bleiben unverändert.
|
||||
|
||||
## Was unterhalb von Stufe 6 gesperrt ist (Keller)
|
||||
## Keller: begehbar ab Stufe 3, Bot-Leads erst ab Stufe 6
|
||||
|
||||
Unterhalb von Stufe 3 (`kellerBegehbar` false) ist der Keller zu:
|
||||
|
||||
- `WorkersPage`: `?tab=botleads` (auch mit `&bot=`) und jeder andere unbekannte Wert zeigen das Hochhaus (`pickTab` nimmt den ersten erlaubten Bildschirm). `BotLeadList` wird nie eingehängt.
|
||||
Die Weiterleitungen `/leads` und `/workers` auf `/hustler` (Query bleibt) gelten weiter und enden dann im Hochhaus.
|
||||
- `HochhausWelt`: die Keller-Etage ist kein Link auf `/hustler?tab=botleads`, sondern ein Knopf wie jede andere Etage (zoomt heran, „Keller heranzoomen“).
|
||||
- `HustlerDialog`: ein Spur-Link in den Keller bleibt Text. Arne ist weiter sichtbar und klickbar (Detailansicht), die Zahl „Bot-Leads“ in der Kopfzeile und am Schild im Haus bleibt.
|
||||
- `workersApi.botleads` ruft unterhalb von Stufe 6 gar nichts ab (kein JWT, keine Anfrage) und liefert den Fehler 404 „Der Keller ist noch nicht freigeschaltet.“
|
||||
|
||||
Ab Stufe 3 sind alle drei Punkte offen (Etage ist ein Link, `?tab=botleads` gilt, Spur-Links führen hinein). Die Kennzahl „Bot-Leads“ in der Kopfzeile ist Text und bleibt es (sie führt auch ab Stufe 6 nirgends hin).
|
||||
|
||||
Unterhalb von Stufe 6 (`botleadsAbrufbar` false) gibt es `GET /botleads` im Dienst noch nicht. Darum:
|
||||
|
||||
- `BotLeadList` (Prop `stufe`, Standard `HUSTLER_STUFE`, von `WorkersPage` durchgereicht) ruft `api.botleads` gar nicht erst auf: kein JWT, keine Anfrage, keine Ladeanzeige, kein Fehlerzustand, kein „Erneut versuchen“.
|
||||
- Der Keller zeigt seine normale Szene (Arne nach seinem Status, Regale, Register-Buch, Raum-Reiter, Suche) im Leer-Zustand mit ehrlichem Text (`RegisterBuch`, Prop `ohneDienst`):
|
||||
**„Noch keine Bot-Leads.“** und darunter „Hier legt Arne Betriebe ohne Website ab, die Bea nicht für eine Mail ausgewählt hat. Die Branchen-Räume (Ärzte, Handwerk, Selbstständige, Läden) suchen noch nicht.“
|
||||
Dieser Text gilt auch mit gewähltem Raum, getippter Suche und in der Einzelansicht (`&bot=`); „Keine Treffer.“ und die Zahl am Reiter gibt es ohne Dienst nicht.
|
||||
- Reiter, Suche, „Register zuklappen“ und „Alle anzeigen“ bleiben bedienbar und ändern nur die Anzeige; sie lösen nie einen Abruf aus und nehmen keinen Fokus weg.
|
||||
- `workersApi.botleads` bleibt unterhalb von Stufe 6 zusätzlich hart gesperrt (zweite Sicherung, falls ein Aufrufer die Stufe nicht prüft): kein JWT, keine Anfrage, Fehler 404 „Die Bot-Leads sind noch nicht freigeschaltet.“ (`BOTLEADS_GESPERRT_TEXT`).
|
||||
|
||||
Ab Stufe 6 gilt das Verhalten mit Live-Daten unverändert (Abruf, Suche mit Verzögerung, Seiten, Akte, Leer-Text „Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.“).
|
||||
|
||||
Es gibt keine Überschreibung zur Laufzeit (kein localStorage, keine Adresse, kein Schalter in der Oberfläche).
|
||||
|
||||
@@ -36,19 +62,19 @@ Es gibt keine Überschreibung zur Laufzeit (kein localStorage, keine Adresse, ke
|
||||
|
||||
1. Den Dienst-Stand live nehmen und prüfen, dass die Endpunkte antworten (P4: ein Pult-Eintrag lässt sich einfügen/beantworten; P6: `GET /api/workers/botleads` antwortet 200).
|
||||
2. In `src/lib/hustlerStufe.js` die `3` in der Zeile `export const HUSTLER_STUFE = stufeAus(buildWert(), 3)` ändern: auf `4` beim Livegang von P4, auf `6` beim Livegang von P6
|
||||
(die Kommentarzeile darüber nennt die Stufen).
|
||||
(die Kommentarzeile darüber nennt die Stufen). Der Keller selbst ist schon mit `3` offen; die `6` schaltet nur seinen Datenabruf ein.
|
||||
3. Tests und Bau wie gewohnt: `node --max-old-space-size=1024 --test tests/*.test.mjs tests/unit/*.test.mjs` und `vite build`.
|
||||
Kein Test hängt an der `3`: `hustlerStufe.test.mjs` liest die Stufe aus dem Quelltext, alle Stufen-Tests geben ihre Stufe (3 und 6) ausdrücklich an. Bricht nach dem Hochsetzen
|
||||
trotzdem ein Test, hat er die Stufe stillschweigend aus dem Standard gelesen und gehört auf eine ausdrückliche Stufe umgestellt.
|
||||
4. Committen, pushen, deployen wie jede andere Änderung. Danach im Browser prüfen: P4 zeigt im Brief „Antwort des Kunden einfügen“ bzw. „Als Mensch antworten“; P6 zeigt die Keller-Etage mit „Keller · Bot-Leads ▸“.
|
||||
4. Committen, pushen, deployen wie jede andere Änderung. Danach im Browser prüfen: P4 zeigt im Brief „Antwort des Kunden einfügen“ bzw. „Als Mensch antworten“; P6 zeigt im Keller das Register mit den Bot-Leads (statt „Noch keine Bot-Leads. Hier legt Arne …“). Die Keller-Etage „Keller · Bot-Leads ▸“ ist schon ab `3` ein Link.
|
||||
|
||||
## Prüfstand (`scripts/workers-harness`)
|
||||
|
||||
Der Prüfstand braucht P4 und Keller für seine Szenarien K und L. `build.mjs` baut deshalb standardmäßig mit `VITE_HUSTLER_STUFE=6`:
|
||||
Der Prüfstand braucht P4 und die Bot-Leads (Live-Daten im Keller) für seine Szenarien K und L. `build.mjs` baut deshalb standardmäßig mit `VITE_HUSTLER_STUFE=6`:
|
||||
|
||||
```
|
||||
node scripts/workers-harness/build.mjs "$(mktemp -d)" # Stufe 6, wie bisher (alle Szenarien)
|
||||
VITE_HUSTLER_STUFE=3 node scripts/workers-harness/build.mjs "$(mktemp -d)" # Stand der Produktion (Szenarien ohne P4 und Keller)
|
||||
VITE_HUSTLER_STUFE=3 node scripts/workers-harness/build.mjs "$(mktemp -d)" # Stand der Produktion (Szenarien ohne P4; der Keller zeigt sein leeres Register)
|
||||
```
|
||||
|
||||
`VITE_HUSTLER_STUFE` gilt nur zur Bauzeit und nur als Ziffer 1 bis 9; alles andere fällt auf die Stufe aus dem Quelltext zurück (Vite) bzw. bricht den Bau des Prüfstands ab.
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
// Drei Module der App hängen an Appwrite (Anmeldung, Mitarbeiterliste, Sitzungs-JWT) und werden nur im Prüfstand ausgetauscht, damit die
|
||||
// echte Seite WorkersPage (Bildschirme Hochhaus, Pult und Keller samt Brief, Akte, ProspectPanel und Einstellungen) ohne Netz läuft: AuthContext -> stub-auth.js,
|
||||
// useEmployees -> stub-employees.js, workersClient -> stub-client.js. Am Quelltext der App ändert das nichts.
|
||||
// Stufen-Sperre (src/lib/hustlerStufe.js): die App zeigt P4-Aktionen im Pult erst ab Stufe 4 und den Keller (Bot-Leads) erst ab Stufe 6. Die Szenarien des Prüfstands
|
||||
// (K pult-p4, L keller, die Keller-Etage im Haus) brauchen beides, deshalb baut dieses Skript standardmäßig mit VITE_HUSTLER_STUFE=6. Eine andere Stufe 1..9 kommt aus der
|
||||
// Umgebung (VITE_HUSTLER_STUFE=3 node scripts/workers-harness/build.mjs <outDir> baut den Stand der Produktion); ein ungültiger Wert bricht ab.
|
||||
// Stufen-Sperre (src/lib/hustlerStufe.js): die App zeigt P4-Aktionen im Pult erst ab Stufe 4 und die Bot-Leads im Keller (Abruf von /botleads) erst ab Stufe 6; der Keller selbst ist
|
||||
// schon ab Stufe 3 begehbar, dann leer. Die Szenarien des Prüfstands (K pult-p4, L keller mit Live-Daten, die Keller-Etage im Haus) brauchen P4 und die Bot-Leads, deshalb baut dieses Skript standardmäßig mit VITE_HUSTLER_STUFE=6. Eine andere Stufe 1..9 kommt aus der
|
||||
// Umgebung (VITE_HUSTLER_STUFE=3 node scripts/workers-harness/build.mjs <outDir> baut den Stand der Produktion, Keller leer); ein ungültiger Wert bricht ab.
|
||||
import esbuild from 'esbuild'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// flock /run/lock/hustler-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 timeout 600 \
|
||||
// node --max-old-space-size=600 scripts/workers-harness/shot.cjs <distDir> <outDir>
|
||||
// <distDir> = Ausgabe von build.mjs, <outDir> = NEUER oder leerer Ordner unter dem Temp-Ordner (TMPDIR, sonst /tmp) oder unter .superpowers/ im Repo.
|
||||
// Stufen-Sperre (src/lib/hustlerStufe.js): build.mjs baut standardmäßig mit VITE_HUSTLER_STUFE=6 (P4-Aktionen im Pult und Keller offen); alle Szenarien unten setzen das voraus.
|
||||
// Stufen-Sperre (src/lib/hustlerStufe.js): build.mjs baut standardmäßig mit VITE_HUSTLER_STUFE=6 (P4-Aktionen im Pult offen, Keller mit Live-Daten); alle Szenarien unten setzen das voraus. Der Keller bei Stufe 3 (leeres Register) ist in botleadList.test.mjs geprüft.
|
||||
// Ein kleiner Server auf 127.0.0.1 liefert distDir und /fonts/* aus public/fonts. Am Ende steht eine JSON-Zeile und `OK`; bei einem Verstoß Exit 1.
|
||||
// Szenarien (Name für HARNESS_ONLY, Vorsilbe der Bilder):
|
||||
// A hochhaus (a-) Hochhaus am Desktop 1280 x 720 und 1920 x 1080: keine Fehler, nichts läuft über den Rand, 19 Figuren mit aria-label, Kim/Pia/Finn grau, Kopfzeile, Briefkasten
|
||||
|
||||
@@ -4,6 +4,7 @@ import KellerSzene from './keller/KellerSzene'
|
||||
import RegisterBuch from './keller/RegisterBuch'
|
||||
import { pageInfo } from '../../lib/botleadsView'
|
||||
import { eintragModell } from '../../lib/hustlerKeller'
|
||||
import { HUSTLER_STUFE, botleadsAbrufbar } from '../../lib/hustlerStufe'
|
||||
import { formatCount } from '../../lib/workersView'
|
||||
import { restoreFocus } from '../../lib/workersFocus'
|
||||
|
||||
@@ -14,15 +15,20 @@ const browserDocument = () => (typeof document === 'undefined' ? null : document
|
||||
|
||||
// Bildschirm „Keller“ (Hustler-Pixelwelt T10, Spec E12/E13; Dateiname bleibt BotLeadList): die Archiv-Szene mit Arne (keller/KellerSzene), davor Arnes Register-Buch
|
||||
// (keller/RegisterBuch) mit den Betrieben, die nicht angeschrieben werden, samt Grund, und die Akte zu einem Eintrag (keller/Akte). Reine Liste, nichts wird gesendet
|
||||
// oder angerufen. Props: api, bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status), paused (Hustler angehalten:
|
||||
// die Szene steht still, Klasse hk-steht), onBack (◂ Hochhaus). Beim Einhängen nimmt die Überschrift „Bot-Leads“ (tabIndex -1) den Fokus, ohne dass die Seite springt (T13 M2).
|
||||
// oder angerufen. Props: api, stufe (Standard HUSTLER_STUFE), bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status),
|
||||
// paused (Hustler angehalten: die Szene steht still, Klasse hk-steht), onBack (◂ Hochhaus). Beim Einhängen nimmt die Überschrift „Bot-Leads“ (tabIndex -1) den Fokus,
|
||||
// ohne dass die Seite springt (T13 M2).
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 6 (BOTLEADS_AB) gibt es GET /botleads im Dienst noch nicht. Der Keller steht dann trotzdem da (Arne, Regale, Register-Buch,
|
||||
// Reiter, Suche), das Register ist leer und sagt ehrlich warum (RegisterBuch, ohneDienst); es gibt keinen Abruf (auch kein JWT), keine Ladeanzeige, keinen Fehler. Reiter und Suche
|
||||
// bleiben bedienbar und ändern nur die Anzeige (es gibt nichts zu filtern); die Bedienung entspricht sonst dem Keller mit Daten, also ohne Fokusfalle.
|
||||
// Die Liste holt sich ihre Daten bei jeder Änderung von Raum, Suche, Seite oder bot selbst; es gibt kein Polling. Nur die jüngste Anfrage zählt: eine späte
|
||||
// Antwort (auch ein später Fehler) einer älteren Anfrage oder nach dem Abbau wird verworfen. Scheitert eine Anfrage, steht der Satz des Dienstes mit „Erneut versuchen“,
|
||||
// das genau diese Anfrage (Raum, Suche, Seite oder Eintrag) noch einmal stellt; Raum und Suche gehen weiterhin.
|
||||
// bot (aus ?bot= der Adresse, Link aus dem Ereignisstrom): zeigt genau diesen Eintrag, hervorgehoben, mit „Alle anzeigen“ (onClearBot nimmt bot aus der Adresse).
|
||||
// Alle Texte der Zeilen sind Textknoten; Links entstehen nur aus row.sourceHref (botleadRow prüft die Adresse mit safeHref: nur http und https, keine Zugangsdaten), und zwar
|
||||
// nur in der Akte. Kein CSS-Import (WorkersPage hängt die Stile ein), keine Bilder, kein HTML-Einschub.
|
||||
export default function BotLeadList({ api, bot, onClearBot, kpis, workers, paused = false, onBack }) {
|
||||
export default function BotLeadList({ api, stufe = HUSTLER_STUFE, bot, onClearBot, kpis, workers, paused = false, onBack }) {
|
||||
const abruf = botleadsAbrufbar(stufe)
|
||||
const botId = typeof bot === 'string' ? bot : ''
|
||||
const single = botId !== ''
|
||||
const [view, setView] = useState(START_VIEW)
|
||||
@@ -45,6 +51,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause
|
||||
const key = JSON.stringify(single ? [botId, attempt] : [view.room, view.q, view.page, attempt])
|
||||
|
||||
useEffect(() => {
|
||||
if (!abruf) return undefined // Stufe unter 6: kein Abruf, das Register bleibt leer
|
||||
seqRef.current += 1
|
||||
const mine = seqRef.current
|
||||
// Ein früheres Ergebnis zu genau diesem Schlüssel (hin und zurück gewechselt, bevor der andere Schlüssel antwortete) ist veraltet: erst die neue Antwort zählt
|
||||
@@ -70,7 +77,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause
|
||||
}
|
||||
load()
|
||||
return () => { seqRef.current += 1 } // eine noch offene Anfrage zählt nicht mehr
|
||||
}, [api, key])
|
||||
}, [api, key, abruf])
|
||||
|
||||
useEffect(() => () => window.clearTimeout(timerRef.current), [])
|
||||
|
||||
@@ -101,7 +108,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause
|
||||
timerRef.current = window.setTimeout(() => setView((v) => ({ ...v, q: value.trim(), page: 1 })), SEARCH_DELAY_MS)
|
||||
}
|
||||
|
||||
const loading = result.key !== key
|
||||
const loading = abruf && result.key !== key // ohne Abruf gibt es nichts zu laden
|
||||
|
||||
// Die Einzelansicht ist weg (die Seite hat bot aus der Adresse genommen): der Raum-Reiter ersetzt den gedrückten Knopf und bekommt den Fokus.
|
||||
// Ohne den Knopf (Zurück-Taste des Browsers) bleibt der Fokus, wo er ist.
|
||||
@@ -159,6 +166,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause
|
||||
draft={draft}
|
||||
filtered={filtered}
|
||||
laden={loading}
|
||||
ohneDienst={!abruf}
|
||||
fehler={result.error}
|
||||
zeilen={zeilen}
|
||||
total={result.total}
|
||||
|
||||
@@ -18,7 +18,7 @@ function Ueberschrift() {
|
||||
// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen), der Bildschirmwechsel (onOpen(tab)) und vonTab (der Bildschirm, von dem man
|
||||
// zurückkommt: 'freigabe' | 'botleads' | null) kommen von der Seite. Beim Zurückkommen bekommt die Etage, über die man gegangen war, den Fokus (M2).
|
||||
// Die Kopfzeile steht im DOM vor der Welt, damit die Tab-Reihenfolge oben beginnt (M7); über der Welt liegt sie per z-index.
|
||||
// stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an die Welt (Keller-Etage als Link nur ab Stufe 6) und an die Detailansicht (Spur-Links in den Keller nur ab Stufe 6).
|
||||
// stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an die Welt (Keller-Etage als Link nur ab Stufe 3) und an die Detailansicht (Spur-Links in den Keller nur ab Stufe 3).
|
||||
// Kein CSS-Import und keine Bilder: die Stile hängt WorkersPage ein (Regeln dieser Datei in styles/hustler/hustler-ui.css).
|
||||
export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null, stufe = HUSTLER_STUFE }) {
|
||||
const wurzelRef = useRef(null)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Himmel, Funkeln, Mond, die 2400 x 1200 große Bühne (per transform verschoben und gezoomt) mit Welt-, Licht-, Haus- und Fensterebenen, Neon, Antenne, Aufzug,
|
||||
// Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee.
|
||||
// Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume.
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) ist die Keller-Etage kein Link auf die Bot-Leads, sondern ein Knopf wie jede andere Etage (heranzoomen).
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb der Stufe, ab der der Keller begehbar ist (3), ist die Keller-Etage kein Link auf den Keller, sondern ein Knopf wie jede andere Etage (heranzoomen).
|
||||
// Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten.
|
||||
// Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist
|
||||
// per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand);
|
||||
@@ -10,7 +10,7 @@
|
||||
// Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML.
|
||||
import { forwardRef, useImperativeHandle } from 'react'
|
||||
import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js'
|
||||
import { HUSTLER_STUFE, kellerOffen } from '../../../lib/hustlerStufe.js'
|
||||
import { HUSTLER_STUFE, kellerBegehbar } from '../../../lib/hustlerStufe.js'
|
||||
import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js'
|
||||
import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js'
|
||||
import useKamera from './useKamera.js'
|
||||
@@ -21,9 +21,9 @@ const LINK_ETAGEN = Object.freeze({
|
||||
keller: Object.freeze({ tab: 'botleads', text: 'Keller · Bot-Leads ▸', label: 'Keller · Bot-Leads: Bot-Leads öffnen' }),
|
||||
})
|
||||
|
||||
// Welche Etagen Links sind: der Keller nur, wenn es die Bot-Leads schon gibt; sonst zoomt er heran wie jede andere Etage
|
||||
// Welche Etagen Links sind: der Keller nur, wenn er begehbar ist (ab Stufe 3, auch wenn sein Register noch leer ist); sonst zoomt er heran wie jede andere Etage
|
||||
const OHNE_KELLER = Object.freeze({ freigabe: LINK_ETAGEN.freigabe })
|
||||
const linkEtagen = (stufe) => (kellerOffen(stufe) ? LINK_ETAGEN : OHNE_KELLER)
|
||||
const linkEtagen = (stufe) => (kellerBegehbar(stufe) ? LINK_ETAGEN : OHNE_KELLER)
|
||||
|
||||
const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width: HAUS_BILD.w, height: HAUS_BILD.h })
|
||||
|
||||
@@ -220,7 +220,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{/* Etagen: Knöpfe zoomen heran, Freigabe (und ab der Keller-Stufe der Keller) sind Links auf die anderen Bildschirme */}
|
||||
{/* Etagen: Knöpfe zoomen heran, Freigabe (und ab der Stufe, ab der der Keller begehbar ist, der Keller) sind Links auf die anderen Bildschirme */}
|
||||
{ETAGEN.map((e) => {
|
||||
const stil = { left: ETAGEN_SPALTE.x, top: e.oben, width: ETAGEN_SPALTE.w, height: e.unten - e.oben }
|
||||
const link = links[e.id]
|
||||
|
||||
@@ -27,7 +27,7 @@ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
||||
|
||||
// Einziger Ort des Dialogs, an dem ein Ziel für einen Link entsteht (wie TraceText in WorkerCard): nur was safeLink durchlässt, wird ein Link
|
||||
// (extern in neuem Tab mit noopener, intern über den Router), alles andere bleibt Text. Stufen-Sperre: ein interner Link in den Keller (?tab=botleads) bleibt
|
||||
// unterhalb der Keller-Stufe (hustlerStufe.js) Text.
|
||||
// unterhalb der Stufe, ab der der Keller begehbar ist (hustlerStufe.js, Stufe 3), Text.
|
||||
function SpurText({ link, stufe, children }) {
|
||||
const safe = isRecord(link) ? safeLink(link.href) : null
|
||||
if (!safe || !linkErlaubt(safe.href, stufe)) return <>{children}</>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
// Das Register-Buch des Kellers (Hustler-Pixelwelt T10, Spec E12/E13): Raum-Reiter, das aufgeschlagene Buch mit Titel, Hinweis, Suchfeld (oder, bei einem einzelnen
|
||||
// Eintrag, „Alle anzeigen“), Kopfzeile, den Einträgen der Dienst-Seite (im Buch scrollbar), Leer-, Lade- und Fehlerzustand und der Seitenzeile mit ◂ ▸.
|
||||
// Aufbau und Klassen wie keller/entwurf/BotLeads.dc.html mit hk- davor; die Inline-Stile der Vorlage stehen als Klassen in styles/hustler/hustler-keller-ui.css.
|
||||
// ohneDienst (Stufe unter 6, der Dienst kennt GET /botleads noch nicht): das Register ist leer und sagt ehrlich warum; weder Fehler noch Ladeanzeige noch Zahl am Reiter.
|
||||
// Rein darstellend: Abruf, Suche, Seiten und Fokus gehören BotLeadList. Alle Texte der Zeilen sind Textknoten; Links gibt es hier nicht (der Link zur Quelle steht in der
|
||||
// Akte und kommt nur aus row.sourceHref). Kein CSS-Import, keine Bilder, kein HTML-Einschub.
|
||||
import { REITER } from '../../../lib/hustlerKeller'
|
||||
import { formatCount } from '../../../lib/workersView'
|
||||
|
||||
const SEARCH_MAX = 100 // der Dienst kürzt die Suche auf 100 Zeichen
|
||||
// Leer-Text, solange der Dienst keine Bot-Leads liefert (Stufe unter 6): Bot-Leads entstehen erst, wenn die Branchen-Räume (P6) suchen
|
||||
const LEER_OHNE_DIENST = [
|
||||
'Noch keine Bot-Leads.',
|
||||
'Hier legt Arne Betriebe ohne Website ab, die Bea nicht für eine Mail ausgewählt hat. Die Branchen-Räume (Ärzte, Handwerk, Selbstständige, Läden) suchen noch nicht.',
|
||||
]
|
||||
|
||||
// Eine Zeile des Buchs: ein Knopf, der die Akte öffnet. Spalten: Raum-Symbol, Name (+ „gekürzt“) mit Ort und Funddatum, Punkte, Grund mit Symbol, Telefon.
|
||||
function Zeile({ z, nr, aktuell, onOpen }) {
|
||||
@@ -42,11 +48,12 @@ function Zeile({ z, nr, aktuell, onOpen }) {
|
||||
|
||||
// Props:
|
||||
// einzeln (Einzelansicht über ?bot=), botId, room (gewählter Raum-Schlüssel), draft (Inhalt des Suchfelds), filtered (Raum oder Suche gesetzt)
|
||||
// ohneDienst (der Dienst liefert noch keine Bot-Leads: ehrlicher Leer-Text)
|
||||
// laden, fehler (Satz des Dienstes oder null), zeilen (eintragModell je Eintrag der Seite), total, info (pageInfo)
|
||||
// onRoom(value), onSearch(ev), onClear(), onPage('prev' | 'next'), onRetry(), onOpen(nr, ev)
|
||||
// reiterRef (am aktiven Reiter), zurueckRef, weiterRef (Blätterknöpfe): Fokus-Ziele von BotLeadList
|
||||
export default function RegisterBuch({ einzeln, botId, room, draft, filtered, laden, fehler, zeilen, total, info, onRoom, onSearch, onClear, onPage, onRetry, onOpen, reiterRef, zurueckRef, weiterRef }) {
|
||||
const zahl = laden || fehler ? null : total // die Zahl steht nur am aktiven Reiter und erst, wenn die Antwort da ist
|
||||
export default function RegisterBuch({ einzeln, botId, room, draft, filtered, ohneDienst = false, laden, fehler, zeilen, total, info, onRoom, onSearch, onClear, onPage, onRetry, onOpen, reiterRef, zurueckRef, weiterRef }) {
|
||||
const zahl = laden || fehler || ohneDienst ? null : total // die Zahl steht nur am aktiven Reiter und erst, wenn die Antwort da ist (ohne Dienst gibt es keine)
|
||||
|
||||
let koerper
|
||||
if (laden) {
|
||||
@@ -59,11 +66,13 @@ export default function RegisterBuch({ einzeln, botId, room, draft, filtered, la
|
||||
</div>
|
||||
)
|
||||
} else if (zeilen.length === 0) {
|
||||
const leer = einzeln
|
||||
? ['Dieser Eintrag ist nicht (mehr) in der Liste.', 'Er wurde gekürzt oder ist kein Bot-Lead.']
|
||||
: filtered
|
||||
? ['Keine Treffer.', 'Für diesen Raum und diese Suche gibt es keine Bot-Leads.']
|
||||
: ['Noch keine Bot-Leads.', 'Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.']
|
||||
const leer = ohneDienst
|
||||
? LEER_OHNE_DIENST
|
||||
: einzeln
|
||||
? ['Dieser Eintrag ist nicht (mehr) in der Liste.', 'Er wurde gekürzt oder ist kein Bot-Lead.']
|
||||
: filtered
|
||||
? ['Keine Treffer.', 'Für diesen Raum und diese Suche gibt es keine Bot-Leads.']
|
||||
: ['Noch keine Bot-Leads.', 'Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.']
|
||||
koerper = (
|
||||
<div className="hk-kl-leer">
|
||||
<p className="hk-kl-leer-titel">{leer[0]}</p>
|
||||
|
||||
+18
-11
@@ -1,14 +1,18 @@
|
||||
// Stufen-Sperre der Hustler-Seite: EINE Quelle der Wahrheit dafür, was die Oberfläche schon zeigen darf. Der Dienst (workers-dienst) wächst Stufe für Stufe
|
||||
// (P1, P3, P4, P6); die Oberfläche ist fertig und würde sonst Knöpfe und Bildschirme zeigen, deren Endpunkte noch 404 liefern.
|
||||
// Stufe < 4: im Pult keine Antwort-Knöpfe (paste „Antwort des Kunden einfügen“, reply „Als Mensch antworten“, approve-preview, start-build).
|
||||
// Stufe < 6: kein Keller mit den Bot-Leads (kein Bildschirm, kein Link, kein Abruf von /api/workers/botleads); die Keller-Etage zoomt wie jede andere.
|
||||
// Stufe < 3: kein Keller (keine Etage als Link, kein Bildschirm, keine Spur-Links in den Keller); die Keller-Etage zoomt wie jede andere.
|
||||
// Stufe >= 3: der Keller ist begehbar (Keller-Etage, ?tab=botleads, Spur-Links): Arne, Regale und Register-Buch stehen da, das Register aber leer, ehrlich beschriftet.
|
||||
// Stufe < 6: der Keller ruft die Bot-Leads nicht ab (kein GET /api/workers/botleads, auch kein JWT); der Endpunkt gibt es im Dienst erst mit P6.
|
||||
// Kein React, kein DOM, kein Appwrite: unter Node testbar (loadModule). Es gibt keine Überschreibung zur Laufzeit (kein localStorage, keine Adresse): die Stufe ist
|
||||
// zur Bauzeit festgelegt. Nur der Prüfstand (scripts/workers-harness) baut mit VITE_HUSTLER_STUFE=6, damit seine P4- und Keller-Szenarien weiter laufen.
|
||||
// zur Bauzeit festgelegt. Nur der Prüfstand (scripts/workers-harness) baut mit VITE_HUSTLER_STUFE=6, damit seine P4- und Keller-Szenarien (mit Live-Daten) weiter laufen.
|
||||
|
||||
/** Ab dieser Stufe erscheinen die Antwort-Knöpfe im Pult (Dienst P4: paste, reply, approve-preview, start-build). */
|
||||
export const STUFE_ANTWORTEN = 4
|
||||
/** Ab dieser Stufe erscheint der Keller mit den Bot-Leads (Dienst P6: GET /botleads). */
|
||||
export const STUFE_KELLER = 6
|
||||
/** Ab dieser Stufe ist der Keller begehbar: Keller-Etage als Link, Bildschirm ?tab=botleads, Spur-Links. Schon mit P3; ohne P6 bleibt das Register leer. */
|
||||
export const KELLER_BEGEHBAR_AB = 3
|
||||
/** Ab dieser Stufe ruft der Keller die Bot-Leads ab (Dienst P6: GET /botleads). Darunter keine Anfrage, auch kein JWT. */
|
||||
export const BOTLEADS_AB = 6
|
||||
|
||||
const KLEINSTE = 1
|
||||
const GROESSTE = 9
|
||||
@@ -33,7 +37,7 @@ function buildWert() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3 = Dienst P3 live; ab 4 (P4) erscheinen die Antwort-Knöpfe im Pult, ab 6 (P6) der Keller mit den Bot-Leads; beim jeweiligen Livegang hier hochsetzen
|
||||
// 3 = Dienst P3 live (der Keller ist begehbar, aber leer); ab 4 (P4) erscheinen die Antwort-Knöpfe im Pult, ab 6 (P6) liefert der Keller die Bot-Leads; beim jeweiligen Livegang hier hochsetzen
|
||||
export const HUSTLER_STUFE = stufeAus(buildWert(), 3)
|
||||
|
||||
// Eine übergebene Stufe (Parameter der Funktionen und Bausteine, vor allem für Tests); fehlt sie oder ist sie unbrauchbar, gilt HUSTLER_STUFE
|
||||
@@ -42,21 +46,24 @@ const stufeOder = (stufe) => stufeAus(stufe, HUSTLER_STUFE)
|
||||
/** Zeigt das Pult die Antwort-Knöpfe (paste, reply, approve-preview, start-build)? Ab Stufe 4. */
|
||||
export const antwortenOffen = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= STUFE_ANTWORTEN
|
||||
|
||||
/** Gibt es den Keller (Bildschirm Bot-Leads, Abruf von /botleads)? Ab Stufe 6. */
|
||||
export const kellerOffen = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= STUFE_KELLER
|
||||
/** Ist der Keller begehbar (Keller-Etage als Link, Bildschirm Bot-Leads, Spur-Links)? Ab Stufe 3. Ob er Daten hat, sagt botleadsAbrufbar. */
|
||||
export const kellerBegehbar = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= KELLER_BEGEHBAR_AB
|
||||
|
||||
/** Darf dieser Bildschirm (Wert von ?tab=) auf der Stufe gezeigt werden? Nur der Keller ('botleads') ist gesperrt, alles andere ist frei. */
|
||||
export const tabErlaubt = (tabId, stufe = HUSTLER_STUFE) => tabId !== 'botleads' || kellerOffen(stufe)
|
||||
/** Darf der Keller die Bot-Leads abrufen (GET /botleads, Dienst P6)? Ab Stufe 6. Darunter zeigt er sein leeres Register und fragt nichts an. */
|
||||
export const botleadsAbrufbar = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= BOTLEADS_AB
|
||||
|
||||
/** Darf dieser Bildschirm (Wert von ?tab=) auf der Stufe gezeigt werden? Nur der Keller ('botleads') ist an die Stufe gebunden (ab 3), alles andere ist frei. */
|
||||
export const tabErlaubt = (tabId, stufe = HUSTLER_STUFE) => tabId !== 'botleads' || kellerBegehbar(stufe)
|
||||
|
||||
// Adressen, unter denen die Hustler-Seite liegt: /hustler und die alten /workers und /leads (App.jsx leitet sie weiter, die Query bleibt)
|
||||
const SEITEN_PFADE = ['/hustler', '/workers', '/leads']
|
||||
|
||||
/**
|
||||
* Darf ein interner Link (aus der Spur im Dialog) auf der Stufe ein Link sein? Ein Link in den Keller (…/hustler?tab=botleads, auch über /workers und /leads)
|
||||
* ist unterhalb der Keller-Stufe nur Text; alles andere bleibt ein Link. Externe Adressen berührt das nicht.
|
||||
* ist unterhalb der Stufe, ab der der Keller begehbar ist (3), nur Text; alles andere bleibt ein Link. Externe Adressen berührt das nicht.
|
||||
*/
|
||||
export function linkErlaubt(href, stufe = HUSTLER_STUFE) {
|
||||
if (kellerOffen(stufe)) return true
|
||||
if (kellerBegehbar(stufe)) return true
|
||||
if (typeof href !== 'string' || !/^\/(?![/\\])/.test(href)) return true // nur interne Pfade (ein einzelner / am Anfang), nie //host oder eine ganze Adresse
|
||||
let ziel
|
||||
try {
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
// Rein: kein Appwrite-Import, getJwt und fetch werden injiziert (workersClient.js verdrahtet sie), damit die
|
||||
// deutschen Fehlertexte unter Node testbar sind. Fehler sind Error mit deutscher `message` und `status`.
|
||||
|
||||
import { HUSTLER_STUFE, kellerOffen } from './hustlerStufe'
|
||||
import { HUSTLER_STUFE, botleadsAbrufbar } from './hustlerStufe'
|
||||
|
||||
export const DOWN_TEXT = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
|
||||
export const BAD_REPLY_TEXT = 'Der Hustler-Dienst antwortet nicht richtig.'
|
||||
export const SESSION_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
||||
export const FORBIDDEN_TEXT = 'Dafür fehlt dir die Berechtigung.'
|
||||
export const INVALID_ID_TEXT = 'Ungültige Kennung.'
|
||||
// Der Keller (GET /botleads) gehört zu Stufe 6 (hustlerStufe.js); darunter gibt es den Endpunkt im Dienst noch nicht
|
||||
export const KELLER_GESPERRT_TEXT = 'Der Keller ist noch nicht freigeschaltet.'
|
||||
// Die Bot-Leads (GET /botleads) gehören zu Stufe 6 (hustlerStufe.js, BOTLEADS_AB); darunter gibt es den Endpunkt im Dienst noch nicht
|
||||
export const BOTLEADS_GESPERRT_TEXT = 'Die Bot-Leads sind noch nicht freigeschaltet.'
|
||||
|
||||
// Zeitgrenze je Abruf (JWT holen, senden, Antwort lesen): ohne sie bliebe ein Abruf auf toter Verbindung (Rechner im Ruhezustand,
|
||||
// Netzwechsel) minutenlang offen und der Feed (ein Abruf je Schleife) stünde still, ohne Fehlermeldung.
|
||||
@@ -198,9 +198,9 @@ export function createWorkersApi({
|
||||
if (!PULT_ACTIONS.includes(action)) throw statusError(`Unbekannte Aktion: ${action}`, 400)
|
||||
return request(`/pult/${pathId(id)}/${action}`, { method: 'POST', body, limitMs: SLOW_PULT_ACTIONS.includes(action) ? actionTimeoutMs : undefined })
|
||||
},
|
||||
// Unterhalb der Keller-Stufe (6) geht gar keine Anfrage hinaus: der Endpunkt fehlt im Dienst noch (die Oberfläche zeigt den Keller dort auch nicht)
|
||||
// Unterhalb der Stufe 6 (BOTLEADS_AB) geht gar keine Anfrage hinaus: der Endpunkt fehlt im Dienst noch (der Keller ruft ihn dort auch nicht auf, er zeigt sein leeres Register)
|
||||
botleads: async ({ room, q, page, id } = {}) => {
|
||||
if (!kellerOffen(stufe)) throw statusError(KELLER_GESPERRT_TEXT, 404)
|
||||
if (!botleadsAbrufbar(stufe)) throw statusError(BOTLEADS_GESPERRT_TEXT, 404)
|
||||
return request(`/botleads${query([['room', room], ['q', q], ['page', page], ['id', id]])}`)
|
||||
},
|
||||
prospect: async (id) => request(`/prospect/${pathId(id)}`),
|
||||
|
||||
@@ -24,8 +24,9 @@ import '../styles/hustler/hustler-regie.css'
|
||||
|
||||
// Bildschirme der Seite: das Hochhaus, das Pult (Freigabe von P3) und der Keller (Bot-Leads von P6). Eine Reiterleiste gibt es nicht mehr;
|
||||
// gewechselt wird im Haus (Etage Freigabe, Etage Keller) und über „◂ Hochhaus“ in Pult und Keller.
|
||||
// Stufen-Sperre (lib/hustlerStufe.js, HUSTLER_STUFE): der Keller ist erst ab Stufe 6 ein Bildschirm. Darunter fallen ?tab=botleads (auch mit &bot=) und jeder
|
||||
// andere unbekannte Wert auf das Hochhaus zurück (pickTab nimmt den ersten Reiter), BotLeadList wird nie eingehängt und /api/workers/botleads nie abgerufen.
|
||||
// Stufen-Sperre (lib/hustlerStufe.js, HUSTLER_STUFE): der Keller ist ab Stufe 3 ein Bildschirm (begehbar), die Bot-Leads ruft er aber erst ab Stufe 6 ab; darunter zeigt
|
||||
// BotLeadList sein leeres Register und fragt /api/workers/botleads nie an. Unterhalb von Stufe 3 fallen ?tab=botleads (auch mit &bot=) und jeder andere unbekannte Wert
|
||||
// auf das Hochhaus zurück (pickTab nimmt den ersten Reiter), BotLeadList wird dann nie eingehängt.
|
||||
const WORKERS_TABS = [
|
||||
{ id: 'hochhaus', label: 'Hochhaus' },
|
||||
{ id: 'freigabe', label: 'Freigabe' },
|
||||
@@ -104,6 +105,7 @@ export default function WorkersPage({ stufe = HUSTLER_STUFE }) {
|
||||
{active.id === 'botleads' && (
|
||||
<BotLeadList
|
||||
api={api}
|
||||
stufe={stufe}
|
||||
bot={bot}
|
||||
onClearBot={() => setParams({ tab: 'botleads' })}
|
||||
kpis={state?.kpis}
|
||||
|
||||
@@ -555,3 +555,148 @@ test('aria-current (C5): nur die Zeile mit der Kennung aus bot, nie eine Zeile o
|
||||
assert.deepEqual(one.rows().map((r) => r.props['aria-current']), [undefined, 'true', undefined])
|
||||
} finally { one.end() }
|
||||
})
|
||||
|
||||
// ---------- Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 6 ist der Keller begehbar, aber leer und ohne Abruf ----------
|
||||
// Der Läufer baut sonst mit stufe: 6 (Live-Daten, alle Tests oben). Hier ausdrücklich Stufe 3 (Produktion: Dienst P3, GET /botleads gibt es noch nicht).
|
||||
|
||||
const OHNE_DIENST_TITEL = 'Noch keine Bot-Leads.'
|
||||
const OHNE_DIENST_TEXT = 'Hier legt Arne Betriebe ohne Website ab, die Bea nicht für eine Mail ausgewählt hat. Die Branchen-Räume (Ärzte, Handwerk, Selbstständige, Läden) suchen noch nicht.'
|
||||
|
||||
test('Stufe 3: der Keller steht mit Szene, Register-Buch, Reitern und Suche da, zeigt den ehrlichen Leer-Text, fragt nichts ab und zeigt weder Laden noch Fehler', async () => {
|
||||
for (const stufe of [3, 4, 5]) {
|
||||
const api = fakeApi()
|
||||
const ui = await start(api, { stufe })
|
||||
try {
|
||||
assert.equal(api.calls.length, 0, `Stufe ${stufe}: kein Abruf`)
|
||||
assert.equal(byClass(ui.tree(), 'hk-kl-szene').length, 1, 'Szene mit Arne')
|
||||
assert.equal(textOf(byClass(ui.tree(), 'hk-kl-titel')[0]), 'Bot-Leads')
|
||||
assert.equal(textOf(byClass(ui.tree(), 'hk-kl-buchtitel')[0]), 'Nie-doppelt-Register')
|
||||
assert.equal(ui.reiter().length, ROOM_VALUES.length); assert.equal(ui.activeIndex(), 0)
|
||||
assert.ok(ui.search(), 'Suchfeld')
|
||||
const leer = byClass(ui.tree(), 'hk-kl-leer')
|
||||
assert.equal(leer.length, 1)
|
||||
assert.equal(textOf(byClass(leer[0], 'hk-kl-leer-titel')[0]), OHNE_DIENST_TITEL)
|
||||
assert.equal(textOf(byClass(leer[0], 'hk-kl-leer-text')[0]), OHNE_DIENST_TEXT)
|
||||
// kein Fehlerzustand, keine Ladeanzeige, kein „Erneut versuchen“, keine Zeilen, keine Kopfzeile, keine Seitenzeile, keine Zahl am Reiter
|
||||
assert.ok(!ui.text().includes('Wird geladen')); assert.equal(byClass(ui.tree(), 'hk-kl-laden').length, 0)
|
||||
assert.equal(ui.errors().length, 0); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0)
|
||||
assert.equal(nodes(ui.tree(), (n) => n.props && (n.props.role === 'alert' || n.props.role === 'status')).length, 0)
|
||||
assert.equal(ui.rows().length, 0); assert.equal(byClass(ui.tree(), 'hk-kopfzeile').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-fuss').length, 0)
|
||||
assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 0)
|
||||
assert.ok(!ui.text().includes('Keine Treffer'))
|
||||
} finally { ui.end() }
|
||||
}
|
||||
})
|
||||
|
||||
test('Stufe 3: ohne Stufen-Angabe gilt HUSTLER_STUFE (Produktion = 3): leeres Register, kein Abruf', async () => {
|
||||
const { HUSTLER_STUFE, botleadsAbrufbar } = await loadModule('src/lib/hustlerStufe.js')
|
||||
const api = fakeApi()
|
||||
const ui = await start(api, { stufe: undefined })
|
||||
try {
|
||||
assert.equal(api.calls.length, botleadsAbrufbar(HUSTLER_STUFE) ? 1 : 0)
|
||||
if (!botleadsAbrufbar(HUSTLER_STUFE)) assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 3: Reiter und Suche bleiben bedienbar, ändern nur die Anzeige und lösen nie einen Abruf aus; die Leer-Auskunft bleibt dieselbe', async () => {
|
||||
const api = fakeApi()
|
||||
const dom = fakeFocus()
|
||||
const ui = await start(api, { stufe: 3 })
|
||||
dom.attach(ui)
|
||||
try {
|
||||
await ui.pick(idx('handwerk'))
|
||||
assert.equal(ui.activeIndex(), idx('handwerk')); assert.equal(dom.focused.length, 0)
|
||||
await ui.type('Elektro'); ui.fireTimers(); await settle()
|
||||
assert.equal(ui.search().props.value, 'Elektro')
|
||||
await ui.pick(idx('laden')); await ui.pick(idx(''))
|
||||
assert.equal(ui.activeIndex(), 0)
|
||||
assert.equal(api.calls.length, 0, 'weder Reiter noch Suche rufen etwas ab')
|
||||
assert.equal(ui.timers.size, 0)
|
||||
assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT))
|
||||
assert.ok(!ui.text().includes('Keine Treffer') && !ui.text().includes('Wird geladen'))
|
||||
assert.equal(ui.errors().length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 0)
|
||||
// keine Fokusfalle: nichts wird dem Nutzer weggenommen oder von der Komponente an sich gezogen (nur die Überschrift beim Einhängen, ohne Seitensprung)
|
||||
assert.ok(dom.focused.every((k) => k !== 'reiter'))
|
||||
} finally { dom.restore(); ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 3: Register zuklappen und aufklappen funktionieren wie mit Daten; ◂ Hochhaus ruft onBack', async () => {
|
||||
const api = fakeApi(); let zurueck = 0
|
||||
const ui = await start(api, { stufe: 3, onBack: () => { zurueck += 1 } })
|
||||
try {
|
||||
await ui.click('Register zuklappen')
|
||||
assert.equal(byClass(ui.tree(), 'hk-buch').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-leer').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-szene').length, 1)
|
||||
await ui.click('Register aufklappen')
|
||||
assert.equal(byClass(ui.tree(), 'hk-kl-leer').length, 1)
|
||||
await ui.click('Zurück zum Hochhaus'); assert.equal(zurueck, 1)
|
||||
assert.equal(api.calls.length, 0)
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 3: ?bot= (Einzelansicht) ruft nichts ab, zeigt dieselbe ehrliche Auskunft und lässt „Alle anzeigen“ zu', async () => {
|
||||
const api = fakeApi(); let leer = 0
|
||||
const ui = await start(api, { stufe: 3, bot: 'p1', onClearBot: () => { leer += 1 } })
|
||||
try {
|
||||
assert.equal(api.calls.length, 0)
|
||||
assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT))
|
||||
assert.ok(!ui.text().includes('Wird geladen') && !ui.text().includes('nicht (mehr) in der Liste'))
|
||||
assert.equal(ui.errors().length, 0); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0)
|
||||
await ui.click('Alle anzeigen'); assert.equal(leer, 1)
|
||||
ui.setProps({ bot: '' }); await settle()
|
||||
assert.equal(api.calls.length, 0); assert.ok(ui.text().includes(OHNE_DIENST_TEXT))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 3: die Zahl im Schild kommt weiter aus den Kennzahlen (kpis.botLeads); die Szene zeigt Arne nach seinem Status', async () => {
|
||||
const api = fakeApi()
|
||||
const arne = (status) => ({ arne: { status, task: '', lastTs: null, connected: true, counters: {} } })
|
||||
const stand = (ui) => ({
|
||||
attr: nodes(ui.tree(), (n) => n.props && n.props['data-hk-arne'] !== undefined).map((n) => n.props['data-hk-arne']),
|
||||
weg: byClass(ui.tree(), 'hk-arne-weg').length, ruht: byClass(ui.tree(), 'hk-kl-ruht').length,
|
||||
alarm: byClass(ui.tree(), 'hk-alarm').length, steht: byClass(ui.tree(), 'hk-steht').length,
|
||||
})
|
||||
const ui = await start(api, { stufe: 3, kpis: { botLeads: 0 }, workers: arne('arbeitet') })
|
||||
try {
|
||||
assert.equal(textOf(byClass(ui.tree(), 'hk-kl-zahl')[0]), '0')
|
||||
// arbeitet: Arnes Ablauf-Schleife läuft, nichts ruht, kein Alarm
|
||||
assert.deepEqual(stand(ui), { attr: ['arbeitet'], weg: 1, ruht: 0, alarm: 0, steht: 0 })
|
||||
// leerlauf: Arne sitzt am Pult, die drei Dinge (Stempel, Rohrpost, Schublade) ruhen
|
||||
ui.setProps({ workers: arne('leerlauf') }); await settle()
|
||||
assert.deepEqual(stand(ui), { attr: ['leerlauf'], weg: 0, ruht: 3, alarm: 0, steht: 0 })
|
||||
// fehler: roter Marker
|
||||
ui.setProps({ workers: arne('fehler') }); await settle()
|
||||
assert.deepEqual(stand(ui), { attr: ['fehler'], weg: 0, ruht: 3, alarm: 1, steht: 0 })
|
||||
// Hustler angehalten: die ganze Szene steht still
|
||||
ui.setProps({ paused: true }); await settle()
|
||||
assert.equal(stand(ui).steht, 1)
|
||||
assert.equal(api.calls.length, 0)
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 6 (und höher): Abruf wie bisher, der Live-Leer-Text bleibt der alte (nicht der der Stufe 3), Lade- und Fehlerzustand sind zurück', async () => {
|
||||
for (const stufe of [6, 9]) {
|
||||
const api = fakeApi()
|
||||
const ui = await start(api, { stufe })
|
||||
try {
|
||||
assert.equal(api.calls.length, 1, `Stufe ${stufe}`); assert.ok(ui.text().includes('Wird geladen'))
|
||||
await ui.answer(0, { items: [], total: 0 })
|
||||
assert.ok(ui.text().includes('Noch keine Bot-Leads.')); assert.ok(ui.text().includes('Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.'))
|
||||
assert.ok(!ui.text().includes(OHNE_DIENST_TEXT)); assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 1)
|
||||
await ui.pick(idx('handwerk'))
|
||||
assert.equal(api.calls.length, 2); await ui.fail(1, new Error('Der Dienst antwortet nicht.'))
|
||||
assert.deepEqual(ui.errors().map((e) => e.text), ['Der Dienst antwortet nicht.']); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 1)
|
||||
} finally { ui.end() }
|
||||
}
|
||||
})
|
||||
|
||||
test('Ändert sich die Stufe von 3 auf 6 (im Betrieb nur über einen neuen Bau, hier als Prop): ab dann Abruf, das Register füllt sich', async () => {
|
||||
const api = fakeApi()
|
||||
const ui = await start(api, { stufe: 3 })
|
||||
try {
|
||||
assert.equal(api.calls.length, 0)
|
||||
ui.setProps({ stufe: 6 }); await settle()
|
||||
assert.equal(api.calls.length, 1); assert.ok(ui.text().includes('Wird geladen'))
|
||||
await ui.answer(0, { items: [ITEM], total: 1 })
|
||||
assert.equal(ui.rows().length, 1); assert.ok(!ui.text().includes(OHNE_DIENST_TEXT))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
@@ -128,7 +128,8 @@ export async function start(api, initial = {}) {
|
||||
setTimeout: (fn, ms) => { const id = nextTimer++; timers.set(id, { fn, ms }); return id },
|
||||
clearTimeout: (id) => { timers.delete(id) },
|
||||
}
|
||||
let props = { api, ...initial }
|
||||
// stufe 6: Live-Daten (Stufen-Sperre, hustlerStufe.js: unterhalb von Stufe 6 ruft der Keller nichts ab und zeigt sein leeres Register); die Tests der Stufe 3 geben sie ausdrücklich an
|
||||
let props = { api, stufe: 6, ...initial }
|
||||
const mounted = __mount(jsx(() => jsx(BotLeadList, props), {}))
|
||||
const view = {
|
||||
timers,
|
||||
|
||||
@@ -385,8 +385,8 @@ test('Spur-Links: unsichere (javascript:, //host, /\\host, data:) bleiben Text;
|
||||
})
|
||||
})
|
||||
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen
|
||||
test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 3 Text und ab Stufe 6 Router-Links; alle anderen Links bleiben', async () => {
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen
|
||||
test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 1 und 2 Text und ab Stufe 3 Router-Links; alle anderen Links bleiben', async () => {
|
||||
const events = [
|
||||
{ id: '1', ts: '2026-10-22T09:00:00Z', text: 'Keller', link: '/hustler?tab=botleads&bot=abc123' },
|
||||
{ id: '2', ts: '2026-10-22T08:59:00Z', text: 'Alt', link: '/workers?tab=botleads&bot=abc123' },
|
||||
@@ -396,7 +396,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s
|
||||
{ id: '6', ts: '2026-10-22T08:55:00Z', text: 'Extern', link: 'https://example.org/hustler?tab=botleads' },
|
||||
]
|
||||
const ziele = (li) => nodes(li, (n) => n.tag === 'a' || n.tag === 'router-link')
|
||||
for (const stufe of [1, 3, 5]) {
|
||||
for (const stufe of [1, 2]) {
|
||||
await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => {
|
||||
const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t)
|
||||
assert.equal(zeilen.length, 6)
|
||||
@@ -407,7 +407,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s
|
||||
assert.ok(!JSON.stringify(ui.find((x) => x.tag === 'a' || x.tag === 'router-link').map((n) => n.props)).includes('botleads&bot'))
|
||||
})
|
||||
}
|
||||
for (const stufe of [6, 9]) {
|
||||
for (const stufe of [3, 5, 6, 9]) {
|
||||
await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => {
|
||||
const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t)
|
||||
assert.deepEqual(ziele(byText('Keller').li).map((n) => n.props.to), ['/hustler?tab=botleads&bot=abc123'])
|
||||
|
||||
@@ -121,7 +121,7 @@ async function start(initial = {}) {
|
||||
const state = initial.state === undefined ? makeState() : initial.state
|
||||
const handle = { current: null }
|
||||
// Stufen-Sperre (hustlerStufe.js): die Zusicherungen dieser Datei gelten für das volle Haus (Stufe 6, Keller offen, Verhalten wie vor der Sperre);
|
||||
// stufe: 3 (Keller gesperrt) steht in den eigenen Tests am Ende des Abschnitts „Etagen“
|
||||
// stufe 1 und 2 (Keller zu) und 3 (Keller begehbar, leer) stehen in den eigenen Tests am Ende des Abschnitts „Etagen“
|
||||
let props = { state, workers: state ? indexWorkers(state) : {}, onZeige() {}, onOpen() {}, stufe: 6, ...initial }
|
||||
const mounted = __mount(jsx(() => jsx(HochhausWelt, { ...props, ref: handle }), {}))
|
||||
await settle()
|
||||
@@ -480,10 +480,10 @@ test('Etagen: ein Etagen-Knopf zoomt heran (Kamera wie kameraEtage), ohne onOpen
|
||||
} finally { ui.end(); restore() }
|
||||
})
|
||||
|
||||
// Stufen-Sperre: unterhalb der Keller-Stufe (6) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar
|
||||
test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => {
|
||||
// Stufen-Sperre: unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar
|
||||
test('Etagen bei Stufe 1 und 2: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => {
|
||||
const { ETAGEN } = await laden()
|
||||
for (const stufe of [1, 3, 5]) {
|
||||
for (const stufe of [1, 2]) {
|
||||
const ui = await start({ stufe })
|
||||
try {
|
||||
const etagen = byClass(ui.tree(), 'hk-etage')
|
||||
@@ -507,11 +507,11 @@ test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist
|
||||
}
|
||||
})
|
||||
|
||||
test('Stufe 3: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => {
|
||||
test('Stufe 1 und 2: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => {
|
||||
const { kameraEtage, ETAGEN } = await laden()
|
||||
const geoeffnet = []
|
||||
const restore = fensterAttrappe()
|
||||
const ui = await start({ stufe: 3, onOpen: (tab) => geoeffnet.push(tab) })
|
||||
const ui = await start({ stufe: 2, onOpen: (tab) => geoeffnet.push(tab) })
|
||||
try {
|
||||
ui.anhaengen()
|
||||
const keller = ETAGEN.find((e) => e.id === 'keller')
|
||||
@@ -540,13 +540,17 @@ test('Stufe 3: Arne bleibt im Haus sichtbar und klickbar (Detail-Dialog), auch d
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 6 und höher: die Keller-Etage bleibt der Link auf die Bot-Leads (wie vor der Sperre)', async () => {
|
||||
for (const stufe of [6, 7, 9]) {
|
||||
const ui = await start({ stufe })
|
||||
// Ab Stufe 3 ist der Keller begehbar (auch ohne Bot-Leads): die Etage ist der Link, genau wie mit Live-Daten ab Stufe 6
|
||||
test('Ab Stufe 3 (Produktion) und höher: die Keller-Etage ist der Link auf den Keller (wie vor der Sperre), Klick wechselt den Bildschirm', async () => {
|
||||
for (const stufe of [3, 4, 5, 6, 7, 9]) {
|
||||
const geoeffnet = []
|
||||
const ui = await start({ stufe, onOpen: (tab) => geoeffnet.push(tab) })
|
||||
try {
|
||||
const keller = ui.etage('Keller · Bot-Leads ▸')
|
||||
assert.equal(keller.tag, 'a', `Stufe ${stufe}`); assert.equal(keller.props.href, '/hustler?tab=botleads')
|
||||
assert.equal(keller.props['aria-label'], 'Keller · Bot-Leads: Bot-Leads öffnen')
|
||||
const e = ereignis(); keller.props.onClick(e)
|
||||
assert.equal(e.defaultPrevented, true, `Stufe ${stufe}`); assert.deepEqual(geoeffnet, ['botleads'], `Stufe ${stufe}`)
|
||||
} finally { ui.end() }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -269,7 +269,7 @@ const loadSeite = () => (seiteBundle ??= esbuild.build({
|
||||
|
||||
// Ein Lauf der Seite. query ist der Teil nach dem ?, feed wird vom Test zwischendurch geändert (setFeed) und die Seite neu gerendert.
|
||||
// stufe (Stufen-Sperre, hustlerStufe.js): die Zusicherungen dieser Datei gelten für die volle Seite (Stufe 6, Keller offen, Verhalten wie vor der Sperre);
|
||||
// die Tests „Stufe 3“ weiter unten bauen die Seite mit stufe: 3 (Stand der Produktion).
|
||||
// die Tests „Stufe 1 und 2“ und „Stufe 3“ weiter unten bauen die Seite mit stufe: 1/2 (Keller zu) und 3 (Stand der Produktion: Keller begehbar, ohne Bot-Leads).
|
||||
async function seite(query, { admin = false, feed, stufe = 6 } = {}) {
|
||||
const { __mount, jsx, WorkersPage } = await loadSeite()
|
||||
const setParamsCalls = []
|
||||
@@ -330,13 +330,13 @@ test('Seite: ?tab=freigabe zeigt das Pult, ?tab=botleads den Keller, beide direk
|
||||
} finally { keller.end() }
|
||||
})
|
||||
|
||||
// ---------- Stufen-Sperre (Stufe 3 = Stand der Produktion: Dienst P3, ohne Keller) ----------
|
||||
// ---------- Stufen-Sperre (Keller begehbar ab Stufe 3 = Stand der Produktion: Dienst P3, Keller ohne Bot-Leads) ----------
|
||||
|
||||
// Adressen, die auf Stufe 3 in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter
|
||||
// Adressen, die in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter
|
||||
const KELLER_ADRESSEN = ['tab=botleads', 'tab=botleads&bot=abc123', 'bot=abc123&tab=botleads', 'tab=botleads&bot=', 'tab=BotLeads', 'ansicht=keller', 'tab=keller', 'tab=botleads&ansicht=keller']
|
||||
|
||||
test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => {
|
||||
for (const stufe of [1, 3, 5]) {
|
||||
test('Stufe 1 und 2: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => {
|
||||
for (const stufe of [1, 2]) {
|
||||
for (const query of KELLER_ADRESSEN) {
|
||||
const ui = await seite(query, { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
@@ -351,8 +351,8 @@ test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der K
|
||||
}
|
||||
})
|
||||
|
||||
test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => {
|
||||
const ui = await seite('tab=freigabe', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
test('Stufe 2: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => {
|
||||
const ui = await seite('tab=freigabe', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
assert.deepEqual(ui.screen(), ['pult'])
|
||||
ui.go('tab=botleads&bot=x'); assert.deepEqual(ui.screen(), ['hochhaus']); assert.equal(ui.hk.props.hochhaus.vonTab, 'freigabe')
|
||||
@@ -362,32 +362,51 @@ test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Frei
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 3: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => {
|
||||
const ui = await seite('tab=hochhaus', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
test('Stufe 2: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => {
|
||||
const ui = await seite('tab=hochhaus', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
const p = ui.hk.props.hochhaus
|
||||
p.onOpen('botleads'); p.onOpen('freigabe')
|
||||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }])
|
||||
ui.go('tab=freigabe')
|
||||
assert.equal(ui.hk.props.pult.stufe, 3)
|
||||
assert.equal(ui.hk.props.pult.stufe, 2)
|
||||
ui.hk.props.pult.onBack()
|
||||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }, { tab: 'hochhaus' }])
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Stufe 6 und höher: ?tab=botleads zeigt den Keller samt Einzelfall (bot), onOpen(\'botleads\') wechselt; Stufe 4 und 5 haben den Keller noch nicht', async () => {
|
||||
for (const stufe of [6, 8, 9]) {
|
||||
// Stufe 3 (Produktion) und höher: der Keller ist ein Bildschirm; die Stufe geht an BotLeadList, die darunter von Stufe 6 entscheidet, ob sie Bot-Leads abruft (botleadList.test.mjs)
|
||||
test('Stufe 3 bis 9: ?tab=botleads (auch alle Schreibweisen mit tab=botleads) zeigt den Keller samt Einzelfall (bot) und bekommt die Stufe; onOpen(\'botleads\') wechselt; Zurück ins Hochhaus', async () => {
|
||||
for (const stufe of [3, 4, 5, 6, 8, 9]) {
|
||||
const ui = await seite('tab=botleads&bot=abc123', { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123')
|
||||
assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123'); assert.equal(ui.hk.props.keller.stufe, stufe)
|
||||
assert.equal(ui.root().props['data-tab'], 'botleads', `Stufe ${stufe}`)
|
||||
ui.hk.props.keller.onBack(); ui.hk.props.keller.onClearBot()
|
||||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }], `Stufe ${stufe}`)
|
||||
ui.go('tab=hochhaus'); ui.hk.props.hochhaus.onOpen('botleads')
|
||||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'botleads' }], `Stufe ${stufe}`)
|
||||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }, { tab: 'botleads' }], `Stufe ${stufe}`)
|
||||
} finally { ui.end() }
|
||||
}
|
||||
for (const stufe of [4, 5]) {
|
||||
const ui = await seite('tab=botleads', { stufe })
|
||||
try { assert.deepEqual(ui.screen(), ['hochhaus'], `Stufe ${stufe}`) } finally { ui.end() }
|
||||
for (const query of ['tab=botleads', 'bot=abc123&tab=botleads', 'tab=botleads&bot=']) {
|
||||
const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try { assert.deepEqual(ui.screen(), ['keller'], query) } finally { ui.end() }
|
||||
}
|
||||
// erfundene Werte bleiben beim Hochhaus, auch auf Stufe 3
|
||||
for (const query of ['tab=BotLeads', 'ansicht=keller', 'tab=keller']) {
|
||||
const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try { assert.deepEqual(ui.screen(), ['hochhaus'], query) } finally { ui.end() }
|
||||
}
|
||||
})
|
||||
|
||||
test('Stufe 3: Hochhaus und Keller bekommen die Stufe, der Keller zählt als Bildschirm der Seite (data-tab); Pause und Kennzahlen gehen an ihn', async () => {
|
||||
const state = makeState({ kpis: { pultOpen: 2, dealsMonth: 1, botLeads: 0 } })
|
||||
const ui = await seite('tab=botleads', { stufe: 3, feed: { state, error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
assert.equal(ui.hk.props.keller.stufe, 3); assert.equal(ui.hk.props.keller.kpis, state.kpis); assert.equal(ui.hk.props.keller.paused, false)
|
||||
ui.go('tab=hochhaus')
|
||||
assert.equal(ui.hk.props.hochhaus.stufe, 3); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Seite: keine Seitenüberschrift, keine Reiterleiste', async () => {
|
||||
|
||||
@@ -7,15 +7,16 @@ import esbuild from 'esbuild'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// Stufen-Sperre der Hustler-Seite (src/lib/hustlerStufe.js): eine Quelle der Wahrheit, was die Oberfläche schon zeigen darf.
|
||||
// Stufe 3 = Dienst P3 live (Stand der Produktion): kein Keller (Bot-Leads, P6), keine Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 der Keller.
|
||||
// Diese Datei prüft das Modul selbst (Stufe aus dem Bau, Schwellen, Bildschirme, Links). Die Bausteine prüfen ihre Sperre in ihren eigenen Tests, jeweils mit Stufe 3 (gesperrt)
|
||||
// und Stufe 6 (offen): rowModel in workersPult.test.mjs, der Client in workersApi.test.mjs, Pult in workersPultUi.test.mjs, Haus in hustlerHochhausWelt.test.mjs, Seite in
|
||||
// hustlerSeite.test.mjs, Spur-Links in hustlerDialog.test.mjs, der Prüfstand in workersHarness.test.mjs.
|
||||
// Stufe 3 = Dienst P3 live (Stand der Produktion): der Keller ist begehbar (Etage, Bildschirm, Spur-Links), aber leer und ohne Abruf der Bot-Leads (P6), und es gibt keine
|
||||
// Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 liefert der Keller die Bot-Leads.
|
||||
// Diese Datei prüft das Modul selbst (Stufe aus dem Bau, Schwellen, Bildschirme, Links). Die Bausteine prüfen ihre Sperre in ihren eigenen Tests, jeweils mit Stufe 3 (Keller leer)
|
||||
// und Stufe 6 (Live-Daten), dazu 1 (Keller zu): rowModel in workersPult.test.mjs, der Client in workersApi.test.mjs, Pult in workersPultUi.test.mjs, Haus in hustlerHochhausWelt.test.mjs,
|
||||
// Seite in hustlerSeite.test.mjs, Keller in botleadList.test.mjs, Spur-Links in hustlerDialog.test.mjs, der Prüfstand in workersHarness.test.mjs.
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8')
|
||||
|
||||
const stufe = await loadModule('src/lib/hustlerStufe.js')
|
||||
const { HUSTLER_STUFE, STUFE_ANTWORTEN, STUFE_KELLER, stufeAus, antwortenOffen, kellerOffen, tabErlaubt, linkErlaubt } = stufe
|
||||
const { HUSTLER_STUFE, STUFE_ANTWORTEN, KELLER_BEGEHBAR_AB, BOTLEADS_AB, stufeAus, antwortenOffen, kellerBegehbar, botleadsAbrufbar, tabErlaubt, linkErlaubt } = stufe
|
||||
|
||||
// ---------- Stufe aus dem Bau ----------
|
||||
|
||||
@@ -36,13 +37,13 @@ test('Standard: ohne Wert aus dem Bau gilt die Stufe aus dem Quelltext, einmal u
|
||||
assert.equal(HUSTLER_STUFE, Number(treffer[0][1]))
|
||||
assert.ok(Number.isInteger(HUSTLER_STUFE) && HUSTLER_STUFE >= 1 && HUSTLER_STUFE <= 9)
|
||||
assert.ok(quelltext.includes('beim jeweiligen Livegang hier hochsetzen'))
|
||||
assert.equal(STUFE_ANTWORTEN, 4); assert.equal(STUFE_KELLER, 6)
|
||||
assert.equal(STUFE_ANTWORTEN, 4); assert.equal(KELLER_BEGEHBAR_AB, 3); assert.equal(BOTLEADS_AB, 6)
|
||||
})
|
||||
|
||||
// Der Wert aus dem Bau (Vite: import.meta.env.VITE_HUSTLER_STUFE, Prüfstand: define von esbuild) wird beim Bauen eingesetzt. Hier derselbe Weg mit esbuild.
|
||||
async function stufeNachBau(define) {
|
||||
const out = await esbuild.build({
|
||||
stdin: { contents: "export { HUSTLER_STUFE, antwortenOffen, kellerOffen } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' },
|
||||
stdin: { contents: "export { HUSTLER_STUFE, antwortenOffen, kellerBegehbar, botleadsAbrufbar } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' },
|
||||
bundle: true, format: 'esm', platform: 'neutral', write: false, define,
|
||||
})
|
||||
return import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`)
|
||||
@@ -52,7 +53,7 @@ test('Bau: VITE_HUSTLER_STUFE überschreibt die Stufe zur Bauzeit (Prüfstand);
|
||||
for (const [roh, erwartet] of [['6', 6], ['4', 4], ['9', 9], ['1', 1], [' 6 ', 6], ['3', 3], ['0', HUSTLER_STUFE], ['10', HUSTLER_STUFE], ['abc', HUSTLER_STUFE], ['', HUSTLER_STUFE], ['6.5', HUSTLER_STUFE], ['-2', HUSTLER_STUFE]]) {
|
||||
const m = await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': JSON.stringify(roh) })
|
||||
assert.equal(m.HUSTLER_STUFE, erwartet, `VITE_HUSTLER_STUFE=${JSON.stringify(roh)}`)
|
||||
assert.equal(m.kellerOffen(), erwartet >= 6); assert.equal(m.antwortenOffen(), erwartet >= 4)
|
||||
assert.equal(m.kellerBegehbar(), erwartet >= 3); assert.equal(m.botleadsAbrufbar(), erwartet >= 6); assert.equal(m.antwortenOffen(), erwartet >= 4)
|
||||
}
|
||||
// nicht gesetzt (Vite: undefined) und ohne import.meta.env (Node): der Standard
|
||||
assert.equal((await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': 'undefined' })).HUSTLER_STUFE, HUSTLER_STUFE)
|
||||
@@ -72,41 +73,48 @@ test('keine Überschreibung zur Laufzeit: kein localStorage, keine Adresse, kein
|
||||
|
||||
// ---------- Schwellen ----------
|
||||
|
||||
test('Schwellen: Antwort-Knöpfe ab Stufe 4, Keller ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => {
|
||||
test('Schwellen: Keller begehbar ab Stufe 3, Antwort-Knöpfe ab Stufe 4, Bot-Leads abrufbar ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => {
|
||||
for (let n = 1; n <= 9; n += 1) {
|
||||
assert.equal(kellerBegehbar(n), n >= 3, `kellerBegehbar(${n})`)
|
||||
assert.equal(antwortenOffen(n), n >= 4, `antwortenOffen(${n})`)
|
||||
assert.equal(kellerOffen(n), n >= 6, `kellerOffen(${n})`)
|
||||
assert.equal(botleadsAbrufbar(n), n >= 6, `botleadsAbrufbar(${n})`)
|
||||
}
|
||||
assert.deepEqual([antwortenOffen(3), kellerOffen(3)], [false, false])
|
||||
assert.deepEqual([antwortenOffen(6), kellerOffen(6)], [true, true])
|
||||
// Stufe 3 (Produktion): Keller begehbar, aber weder Antwort-Knöpfe noch Abruf der Bot-Leads; der Abruf setzt die Begehbarkeit voraus
|
||||
assert.deepEqual([kellerBegehbar(3), antwortenOffen(3), botleadsAbrufbar(3)], [true, false, false])
|
||||
assert.deepEqual([kellerBegehbar(2), antwortenOffen(2), botleadsAbrufbar(2)], [false, false, false])
|
||||
assert.deepEqual([kellerBegehbar(6), antwortenOffen(6), botleadsAbrufbar(6)], [true, true, true])
|
||||
assert.ok(KELLER_BEGEHBAR_AB <= BOTLEADS_AB)
|
||||
for (const kaputt of [undefined, null, 'x', 0, 12, NaN]) {
|
||||
assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(kaputt), kellerOffen(HUSTLER_STUFE))
|
||||
assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(kaputt), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(kaputt), botleadsAbrufbar(HUSTLER_STUFE))
|
||||
}
|
||||
assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(), kellerOffen(HUSTLER_STUFE))
|
||||
assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(), botleadsAbrufbar(HUSTLER_STUFE))
|
||||
})
|
||||
|
||||
// ---------- Bildschirme und Deeplinks ----------
|
||||
|
||||
test('tabErlaubt: nur der Keller (botleads) ist gesperrt, und nur unterhalb von Stufe 6', () => {
|
||||
for (const s of [1, 3, 4, 5]) {
|
||||
test('tabErlaubt: nur der Keller (botleads) hängt an der Stufe, und nur unterhalb von Stufe 3 ist er gesperrt (ab 3 begehbar, auch ohne Bot-Leads)', () => {
|
||||
for (const s of [1, 2]) {
|
||||
assert.equal(tabErlaubt('botleads', s), false, `Stufe ${s}`)
|
||||
for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`)
|
||||
}
|
||||
for (const s of [6, 7, 9]) assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`)
|
||||
for (const s of [3, 4, 5, 6, 7, 9]) {
|
||||
assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`)
|
||||
for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 6 kein Link, alles andere bleibt unberührt', () => {
|
||||
test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 3 kein Link und ab 3 einer, alles andere bleibt unberührt', () => {
|
||||
const keller = [
|
||||
'/hustler?tab=botleads', '/hustler?tab=botleads&bot=abc123', '/hustler/?tab=botleads', '/Hustler?tab=botleads', '/hustler?bot=x&tab=botleads',
|
||||
'/workers?tab=botleads&bot=x', '/leads?tab=botleads', '/hustler?tab=botleads#oben',
|
||||
]
|
||||
for (const href of keller) {
|
||||
assert.equal(linkErlaubt(href, 3), false, `Stufe 3: ${href}`); assert.equal(linkErlaubt(href, 5), false, `Stufe 5: ${href}`)
|
||||
assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${href}`)
|
||||
assert.equal(linkErlaubt(href, 1), false, `Stufe 1: ${href}`); assert.equal(linkErlaubt(href, 2), false, `Stufe 2: ${href}`)
|
||||
for (const s of [3, 5, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${href}`)
|
||||
}
|
||||
const frei = [
|
||||
'/hustler?tab=freigabe', '/hustler', '/hustler?tab=hochhaus', '/hustler?bot=abc123', '/tickets/100001', '/customers/abc', '/other?tab=botleads', '/hustler/anderes?tab=botleads',
|
||||
'https://beispiel.de/hustler?tab=botleads', 'http://x.example/?tab=botleads', '//evil.example/hustler?tab=botleads', '/\\evil.example/hustler?tab=botleads', '', 'kein-pfad', null, undefined, 42,
|
||||
]
|
||||
for (const href of frei) { assert.equal(linkErlaubt(href, 3), true, `Stufe 3: ${String(href)}`); assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${String(href)}`) }
|
||||
for (const href of frei) { for (const s of [1, 3, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${String(href)}`) }
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
|
||||
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, KELLER_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js')
|
||||
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, BOTLEADS_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js')
|
||||
const { actionErrorText, UNCERTAIN_TEXT } = await loadModule('src/lib/workersPult.js')
|
||||
const { createJwtCache } = await loadModule('src/lib/serverpulsAuth.js')
|
||||
|
||||
@@ -707,15 +707,14 @@ test('createJWT: Übertragungsfehler beim frischen JWT nach einer 401 sperrt nic
|
||||
assert.deepEqual(mints, [false, true, false, true]) // keine Sperre: weder für frische JWTs noch für createJWT überhaupt
|
||||
})
|
||||
|
||||
// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf des Kellers unterhalb von Stufe 6 ----------
|
||||
|
||||
// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf der Bot-Leads unterhalb von Stufe 6 (BOTLEADS_AB; der Keller selbst ist schon ab Stufe 3 begehbar, dann leer) ----------
|
||||
|
||||
test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch kein JWT), auf Stufe 6 wie bisher', async () => {
|
||||
for (const s of [1, 3, 5]) {
|
||||
for (const s of [1, 3, 4, 5]) {
|
||||
const f = fakeFetch(okReplies(8)); let jwtGeholt = 0
|
||||
const api = createWorkersApi({ getJwt: async () => { jwtGeholt += 1; return 'jwt' }, fetchImpl: f, stufe: s })
|
||||
for (const params of [undefined, {}, { room: 'handwerk' }, { q: 'Bäcker', page: 2 }, { id: 'abc' }]) {
|
||||
await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === KELLER_GESPERRT_TEXT, `Stufe ${s}`)
|
||||
await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === BOTLEADS_GESPERRT_TEXT, `Stufe ${s}`)
|
||||
}
|
||||
assert.equal(f.calls.length, 0, `Stufe ${s}`); assert.equal(jwtGeholt, 0, `Stufe ${s}`)
|
||||
// die übrigen Abrufe der Stufe 3 laufen
|
||||
@@ -728,5 +727,5 @@ test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch ke
|
||||
await api.botleads({ room: 'handwerk', page: 2 })
|
||||
assert.deepEqual(f.calls.map((c) => [c.init.method || 'GET', c.url]), [['GET', '/api/workers/botleads?room=handwerk&page=2']])
|
||||
}
|
||||
assert.match(KELLER_GESPERRT_TEXT, /Keller/)
|
||||
assert.match(BOTLEADS_GESPERRT_TEXT, /Bot-Leads/)
|
||||
})
|
||||
@@ -19,7 +19,7 @@ test('WorkersPage: Tab-Titel Hustler, drei Bildschirme in fester Reihenfolge, ke
|
||||
assert.match(s, /id: 'hochhaus', label: 'Hochhaus'/); assert.match(s, /id: 'freigabe', label: 'Freigabe'/); assert.match(s, /id: 'botleads', label: 'Bot-Leads'/)
|
||||
assert.ok(s.indexOf("id: 'hochhaus'") < s.indexOf("id: 'freigabe'") && s.indexOf("id: 'freigabe'") < s.indexOf("id: 'botleads'"))
|
||||
assert.doesNotMatch(s, /title="Hustler"/); assert.doesNotMatch(s, /PageHeader/); assert.doesNotMatch(s, /<Tabs\b/); assert.doesNotMatch(s, /WORKERS_TABS\.length > 1/)
|
||||
assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(WORKERS_TABS\.filter\(\(t\) => tabErlaubt\(t\.id, stufe\)\), params\.get\('tab'\)\)/); assert.match(s, /useAuth\(\)/) // Stufen-Sperre: der Keller ist nur ab Stufe 6 ein Bildschirm (hustlerStufe.test.mjs)
|
||||
assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(WORKERS_TABS\.filter\(\(t\) => tabErlaubt\(t\.id, stufe\)\), params\.get\('tab'\)\)/); assert.match(s, /useAuth\(\)/) // Stufen-Sperre: der Keller ist ab Stufe 3 ein Bildschirm, die Bot-Leads ruft er erst ab Stufe 6 ab (hustlerStufe.test.mjs, botleadList.test.mjs)
|
||||
assert.match(s, /<Hochhaus[^>]*\bapi=\{api\}/); assert.match(s, /<Hochhaus[^>]*isAdmin=\{isAdmin\}/)
|
||||
assert.match(s, /<div className="hk-seite" data-tab=\{active\.id\}>/) // Vollhöhen-Rahmen, data-tab schaltet den Übergang für Pult und Keller
|
||||
assert.match(s, /import Hochhaus from '\.\.\/components\/workers\/hochhaus\/Hochhaus'/)
|
||||
@@ -39,7 +39,7 @@ test('WorkersPage: freigabe zeigt die PultList, botleads die BotLeadList (api, b
|
||||
assert.match(s, /import PultList from '\.\.\/components\/workers\/PultList'/); assert.match(s, /import BotLeadList from '\.\.\/components\/workers\/BotLeadList'/)
|
||||
assert.match(s, /active\.id === 'freigabe' && \([\s\S]*?<PultList[^>]*\bapi=\{api\}/); assert.match(s, /<PultList[^>]*me=\{\{ id: user\?\.\$id, name: user\?\.name \}\}/)
|
||||
assert.match(s, /active\.id === 'botleads' && \([\s\S]*?<BotLeadList[^>]*\bapi=\{api\}/)
|
||||
assert.match(s, /params\.get\('bot'\)/); assert.match(s, /<BotLeadList[^>]*\bbot=\{bot\}/); assert.match(s, /<BotLeadList[^>]*onClearBot=\{\(\) => setParams\(\{ tab: 'botleads' \}\)\}/)
|
||||
assert.match(s, /params\.get\('bot'\)/); assert.match(s, /<BotLeadList[^>]*\bstufe=\{stufe\}/); assert.match(s, /<BotLeadList[^>]*\bbot=\{bot\}/); assert.match(s, /<BotLeadList[^>]*onClearBot=\{\(\) => setParams\(\{ tab: 'botleads' \}\)\}/)
|
||||
assert.match(s, /onOpen=\{\(tab\) => \{ if \(tabErlaubt\(tab, stufe\)\) setParams\(\{ tab \}\) \}\}/) // setParams ersetzt alle Parameter: bot fällt beim Wechsel weg; ein gesperrter Bildschirm öffnet sich nie
|
||||
assert.equal(s.match(/onBack=\{\(\) => setParams\(\{ tab: 'hochhaus' \}\)\}/g).length, 2) // „◂ Hochhaus“ in Pult und Keller
|
||||
for (const name of ['PultList', 'BotLeadList']) assert.match(s, new RegExp(`<${name}[\\s\\S]*?kpis=\\{state\\?\\.kpis\\}[\\s\\S]*?workers=\\{workers\\}[\\s\\S]*?paused=\\{paused\\}[\\s\\S]*?/>`)) // Pause (R3) geht an Pult und Keller
|
||||
|
||||
Reference in new issue
Block a user