From 2652108e4c8ea64f7ab7c458defe10399b658895 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 10:36:52 +0000 Subject: [PATCH] =?UTF-8?q?Hustler-Stufe:=20Pr=C3=BCfstand=20baut=20mit=20?= =?UTF-8?q?VITE=5FHUSTLER=5FSTUFE=3D6,=20Notiz=20zum=20Hochsetzen=20beim?= =?UTF-8?q?=20Livegang=20von=20P4=20und=20P6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/superpowers/hustler-stufe.md | 55 ++++++++++++++++++++++++++++++ scripts/workers-harness/build.mjs | 13 +++++-- scripts/workers-harness/shot.cjs | 1 + tests/unit/workersHarness.test.mjs | 38 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/hustler-stufe.md diff --git a/docs/superpowers/hustler-stufe.md b/docs/superpowers/hustler-stufe.md new file mode 100644 index 0000000..437e408 --- /dev/null +++ b/docs/superpowers/hustler-stufe.md @@ -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. diff --git a/scripts/workers-harness/build.mjs b/scripts/workers-harness/build.mjs index 7ed7ff5..470793d 100644 --- a/scripts/workers-harness/build.mjs +++ b/scripts/workers-harness/build.mjs @@ -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 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}`) diff --git a/scripts/workers-harness/shot.cjs b/scripts/workers-harness/shot.cjs index 9dcbdd3..de5269b 100644 --- a/scripts/workers-harness/shot.cjs +++ b/scripts/workers-harness/shot.cjs @@ -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 // = Ausgabe von build.mjs, = 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 diff --git a/tests/unit/workersHarness.test.mjs b/tests/unit/workersHarness.test.mjs index 6a28989..6371792 100644 --- a/tests/unit/workersHarness.test.mjs +++ b/tests/unit/workersHarness.test.mjs @@ -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, '') + } +})