Hustler-Stufe: Prüfstand baut mit VITE_HUSTLER_STUFE=6, Notiz zum Hochsetzen beim Livegang von P4 und P6

build.mjs setzt die Stufe über define ein (Standard 6, andere Ziffer 1 bis 9 aus der Umgebung, Ungültiges bricht ab), damit die Szenarien K (Pult P4) und L (Keller) weiter laufen;
ein Test baut den Prüfstand mit Standard, 3 und ungültigen Werten. docs/superpowers/hustler-stufe.md erklärt Stufen, Sperren und die Schritte beim Hochsetzen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-22 10:36:52 +00:00
1 parent 32f4f1d941
commit 2652108e4c
4 files changed
+105 -2

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
# Hustler-Stufe: was die Oberfläche schon zeigen darf
Der Hustler-Dienst (`knso/workers-dienst`) geht Stufe für Stufe live, die Oberfläche (WOMS, `/hustler`) ist schon fertig. Damit Mitarbeiter keine Knöpfe und Bildschirme sehen,
deren Endpunkte im Dienst noch fehlen (404 `{"error":"Nicht gefunden."}`), gibt es eine **Stufen-Sperre** mit genau einer Quelle der Wahrheit:
```
src/lib/hustlerStufe.js
export const HUSTLER_STUFE = stufeAus(buildWert(), 3)
```
Die `3` in dieser Zeile ist die Stufe der Produktion. Mehr ist zum Hochsetzen nicht zu ändern.
| 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`. |
## Was unterhalb von Stufe 4 gesperrt ist (P4-Aktionen)
- `rowModel` (`src/lib/workersPult.js`, Parameter `stufe`, Standard `HUSTLER_STUFE`) liefert `canReply`, `canPaste`, `canApprovePreview` und `canStartBuild` immer als `false`.
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)
- `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.“
Es gibt keine Überschreibung zur Laufzeit (kein localStorage, keine Adresse, kein Schalter in der Oberfläche).
## Stufe beim Livegang hochsetzen
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).
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 ▸“.
## 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`:
```
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` 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.
**Die Variable gehört nicht in den Produktions-Bau** (nicht in `.env`, nicht in Build-Argumente): die Produktion richtet sich allein nach dem Quelltext.
+11 -2
View File
@@ -5,6 +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.
import esbuild from 'esbuild'
import fs from 'node:fs'
import os from 'node:os'
@@ -32,6 +35,12 @@ if (fs.existsSync(out) && fs.readdirSync(out).length > 0) {
}
fs.mkdirSync(out, { recursive: true })
const stufe = process.env.VITE_HUSTLER_STUFE ?? '6'
if (!/^[1-9]$/.test(stufe)) {
console.error(`refusing: VITE_HUSTLER_STUFE must be a digit 1..9 (got ${JSON.stringify(stufe)})`)
process.exit(2)
}
const STUBS = [
{ filter: /(^|\/)context\/AuthContext(\.jsx?)?$/, file: 'stub-auth.js' },
{ filter: /(^|\/)hooks\/useEmployees(\.jsx?)?$/, file: 'stub-employees.js' },
@@ -57,10 +66,10 @@ const result = await esbuild.build({
// Bilder der Pixelwelt (url() in src/styles/hustler/*.css) landen als data:-URI im harness.css
loader: { '.png': 'dataurl' },
plugins: [stubPlugin],
define: { 'process.env.NODE_ENV': '"development"' },
define: { 'process.env.NODE_ENV': '"development"', 'import.meta.env.VITE_HUSTLER_STUFE': JSON.stringify(stufe) },
logLevel: 'warning',
metafile: true,
})
fs.copyFileSync(path.join(here, 'index.html'), path.join(out, 'index.html'))
const sizes = Object.entries(result.metafile.outputs).map(([file, info]) => `${path.basename(file)} ${info.bytes}`)
console.log(`built ${sizes.join(', ')} -> ${out}`)
console.log(`built ${sizes.join(', ')} (Stufe ${stufe}) -> ${out}`)
+1
View File
@@ -3,6 +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.
// 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
+38
View File
@@ -1,5 +1,10 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadModule } from './load.mjs'
// Prüfstand der Hustler-Seite: die Skript-API (gleiche Form wie createWorkersApi) für die manuelle Playwright-Abnahme.
@@ -189,3 +194,36 @@ test('Fetch-Weiche hang: die Antwort kommt nie, der API-Client gibt nach der Zei
const { api } = wired('hang', { timeoutMs: 40 })
await assert.rejects(api.state(), { message: 'Der Hustler-Dienst ist gerade nicht erreichbar.', status: 0 })
})
// ---------- Stufen-Sperre (hustlerStufe.js): der Prüfstand baut mit Stufe 6 ----------
test('Prüfstand: build.mjs baut standardmäßig mit Stufe 6, nimmt eine andere Stufe 1..9 aus VITE_HUSTLER_STUFE und lehnt Ungültiges ab', () => {
const tmp = fs.realpathSync(os.tmpdir())
const build = fileURLToPath(new URL('../../scripts/workers-harness/build.mjs', import.meta.url))
const lauf = (wert) => {
const dir = fs.mkdtempSync(path.join(tmp, 'hk-stufe-'))
assert.ok(dir.startsWith(tmp + path.sep))
try {
const env = { ...process.env, TMPDIR: tmp }
delete env.VITE_HUSTLER_STUFE
if (wert !== undefined) env.VITE_HUSTLER_STUFE = wert
let ausgabe = ''; let code = 0
try {
ausgabe = execFileSync(process.execPath, ['--max-old-space-size=512', build, dir], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 120000 })
} catch (err) {
code = err.status ?? 1
}
return { code, ausgabe, js: fs.existsSync(path.join(dir, 'harness.js')) ? fs.readFileSync(path.join(dir, 'harness.js'), 'utf8') : '' }
} finally {
fs.rmSync(dir, { recursive: true, force: true })
}
}
const standard = lauf(undefined)
assert.equal(standard.code, 0); assert.match(standard.ausgabe, /\(Stufe 6\)/); assert.match(standard.js, /function buildWert\(\) \{\s*try \{\s*return "6";/, 'Stufe 6 ist in den Prüfstand eingesetzt')
const produktion = lauf('3')
assert.equal(produktion.code, 0); assert.match(produktion.ausgabe, /\(Stufe 3\)/); assert.match(produktion.js, /function buildWert\(\) \{\s*try \{\s*return "3";/)
for (const ungueltig of ['0', '10', 'abc', '']) {
const r = lauf(ungueltig)
assert.equal(r.code, 2, `VITE_HUSTLER_STUFE=${JSON.stringify(ungueltig)}`); assert.equal(r.js, '')
}
})