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:
1 parent
32f4f1d941
commit
2652108e4c
4 files changed
+105
-2
No files matched your search
@@ -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.
|
||||||
@@ -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
|
// 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,
|
// 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.
|
// 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 esbuild from 'esbuild'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
@@ -32,6 +35,12 @@ if (fs.existsSync(out) && fs.readdirSync(out).length > 0) {
|
|||||||
}
|
}
|
||||||
fs.mkdirSync(out, { recursive: true })
|
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 = [
|
const STUBS = [
|
||||||
{ filter: /(^|\/)context\/AuthContext(\.jsx?)?$/, file: 'stub-auth.js' },
|
{ filter: /(^|\/)context\/AuthContext(\.jsx?)?$/, file: 'stub-auth.js' },
|
||||||
{ filter: /(^|\/)hooks\/useEmployees(\.jsx?)?$/, file: 'stub-employees.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
|
// Bilder der Pixelwelt (url() in src/styles/hustler/*.css) landen als data:-URI im harness.css
|
||||||
loader: { '.png': 'dataurl' },
|
loader: { '.png': 'dataurl' },
|
||||||
plugins: [stubPlugin],
|
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',
|
logLevel: 'warning',
|
||||||
metafile: true,
|
metafile: true,
|
||||||
})
|
})
|
||||||
fs.copyFileSync(path.join(here, 'index.html'), path.join(out, 'index.html'))
|
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}`)
|
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}`)
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// flock /run/lock/hustler-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 timeout 600 \
|
// 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>
|
// 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.
|
// <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.
|
// 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):
|
// 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
|
// 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
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
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'
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
// Prüfstand der Hustler-Seite: die Skript-API (gleiche Form wie createWorkersApi) für die manuelle Playwright-Abnahme.
|
// 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 })
|
const { api } = wired('hang', { timeoutMs: 40 })
|
||||||
await assert.rejects(api.state(), { message: 'Der Hustler-Dienst ist gerade nicht erreichbar.', status: 0 })
|
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, '')
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user