Files
tickte-system/docs/superpowers/plans/2026-10-17-workers-00-uebersicht.md
T
2026-10-17 17:10:25 +00:00

35 KiB
Raw Blame History

Workers – Plan-Übersicht und gemeinsame Schnittstellen

Dieses Dokument ist die verbindliche Quelle für alle Workers-Teilpläne P1–P6. Jeder Teilplan verweist hierauf und darf die hier festgelegten Namen, Felder, Pfade und Signaturen nicht abweichend verwenden. Wer etwas ändern muss, ändert es hier und in allen betroffenen Plänen.

Spec: docs/superpowers/specs/2026-10-17-workers-design.md (freigegeben 17.10.2026) Code-Landkarten (Fakten zum Bestand): /root/reports/workers-2026-10-16/codekarten/{woms,portal,infra,mail-claude,daten-spiel}.md Pixel-Vorlage: /root/reports/workers-2026-10-16/entwuerfe/{sprites.js,roster.js,scene-turm.js,index.html,smoke.js,shot.cjs}


1. Teilpläne, Reihenfolge, Orte

Plan Datei Inhalt Repo / Arbeitsort Hängt ab von
P1 Dienst-Fundament 2026-10-17-workers-p1-dienst-fundament.md Dienst-Gerüst, Appwrite-Sammlungen + Setup-Skript, Normalisierung + Nie-doppelt-Register + Startbestand, Ereignisstrom, Budget-Wächter + Claude-Hülle, Rollen-Register mit Werkzeug-/Kanal-Sperre, Zeitplan-Tick, Otto (Nachtplan + Morgenbericht), HTTP-API, Container + Betrieb neues Repo knso/workers-dienst –
P2 Hochhaus in WOMS 2026-10-17-workers-p2-hochhaus-woms.md Menüpunkt + Route /workers, WorkersPage, Pixel-Engine (Port aus der Vorlage, Etagen/Räume als Daten, Lift, Ereignis-Regie, Leerlauf-/Grau-Zustände), Polling-Feed, Worker-Karte mit Spur, Kennzahlen, Pausenschalter, Einstellungen (Admin) WOMS JUSN/tickte-system, Worktree dev-workers, Branch feature/workers P1 (API)
P3 Etage „Anfragen“ + Freigabe-Pult 2026-10-17-workers-p3-anfragen-pult.md Rita/Kim/Pia/Finn, Alarm-Postfach lesen, Bea-Bewertung für Anfragen, Clara-Erstantwort, Pult im Dienst + Pult-UI in WOMS Dienst + WOMS P1, P2
P4 Gespräche, Werkstatt, Prüfung 2026-10-17-workers-p4-gespraeche-werkstatt.md Max als einziger Absender (SMTP-Tor), Nora-Posteingang (IMAP + Einordnung), Fäden, Theo, Bob/Bianca/Ben-Bau, Paul-Prüfung mit Screenshots, Vorschau-Server entwurf.project.webklar.com, Vorschau-Freigabe, Erinnerungen Dienst (+ WOMS-Pult-Erweiterung) P1–P3
P5 Kaufweg im Portal 2026-10-17-workers-p5-portal-kaufweg.md „Website übernehmen“-Seite + Routen ohne Login, Stripe-Checkout ohne Kunde, Webhook-Zweig legt Kunde an, Nachweis, K44-Mail, Vorschau → Kundenprojekt, Status zurück an Workers Portal knso/Webklar-Kundenbereich, Worktree dev-workers-kauf, Branch feature/workers-kaufweg P1, P4 (Angebots-Doc)
P6 Etage „Ohne Website“ + Kaltmail 2026-10-17-workers-p6-ohne-website.md Ali/Hanna/Sven/Lea (Places/OSM/Impressum), Top-3 über alle Räume, Bot-Lead-Liste (Dienst + WOMS-Ansicht), Max-Kaltmail mit allen Pflichtangaben, Nachfassen, Aufbewahrung/Kürzung, Datenschutz-Abschnitt „Interessenten“ Dienst + WOMS + webklar.com-Rechtstext P1–P4

Reihenfolge der Umsetzung: P1 → P2 → P3 → P4 → P5 → P6. P5 kann nach P1 parallel zu P3/P4 laufen (eigene Dateien im Portal), solange das Angebots-Dokument aus §4.12 feststeht.

1.1 Repos, Checkouts, Branches

Was Ort Regel
Workers-Dienst (neu) Gitea knso/workers-dienst (nicht in der Org WEBklar: deren Org-Webhook legt für jeden Push ein websiteProjects-Dokument an). Entwicklung: /home/webklar/apps/workers-dev (Branch je Plan: p1-fundament, p3-anfragen, p4-gespraeche, p6-ohne-website). Betrieb: /home/webklar/apps/workers (Branch main). Repo anlegen per Gitea-MCP create_repo (privat). Kenso merged/pusht nach main.
WOMS /home/webklar/apps/ticket.webklar.com/dev-workers, Branch feature/workers Live geht nur, wenn Kenso git push origin feature/workers:test ausführt.
Portal Worktree /home/webklar/apps/kundenbereich.webklar.com/dev-workers-kauf von origin/main, Branch feature/workers-kaufweg Live: Kenso pusht nach main (Deploy-Webhook).

2. Globale Regeln (gelten in jedem Task jedes Plans)

  1. Speicher: Jeder Node-Lauf (Tests, Skripte) mit timeout <s> node --max-old-space-size=1024 …. Vite/esbuild-Bündel nur unter systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0. Playwright/Chromium nur unter systemd-run --scope -q -p MemoryMax=700M -p MemorySwapMax=0. Vor schweren Läufen free -m.
  2. Sicherheit beim Ausführen: kein rm -rf außerhalb eigener Scratch-Ordner unter $TMPDIR; nie /dev, /etc, / anfassen; kein docker run, keine globalen Installs; Prozesse nur per PID beenden (pkill -f trifft die eigene Shell); grep ist ein Wrapper → command grep -a oder rg.
  3. Git: nur git add <dateien> mit Namen, nie -A/.. Agenten pushen nie und führen kein docker compose up aus; das macht Kenso nach einer Betriebsanleitung im Plan. .env und docker-compose.yml unter /home/webklar/apps sind nicht in Git → vor jeder Änderung Backup *.bak-YYYY-MM-DD-workers.
  4. WOMS-Worktree: node_modules fehlt → einmal ln -s ../build/node_modules node_modules (zeigt als ?? node_modules, nie committen). Tests: timeout 600 node --max-old-space-size=1024 --test tests/*.test.mjs tests/unit/*.test.mjs; reine Libs unter src/lib/ ohne React/Appwrite-Import, geladen mit loadModule() aus tests/unit/load.mjs.
  5. Portal: Tests timeout 900 node --max-old-space-size=1024 --test test/<datei>.test.js mit startAppwriteMock/startFakeStripe/mailSink (Muster test/aboRoutes.test.js).
  6. Dienst: Node 22 (Container node:22-alpine), ESM, keine Semikolons, deutsche Kommentare, Tests timeout 300 node --max-old-space-size=1024 --test test/*.test.js. Jede äußere Abhängigkeit (Appwrite, Anthropic, SMTP, IMAP, Uhr) wird per Parameter injiziert, damit Tests ohne Netz laufen. In Tests nie echte Anthropic-, SMTP- oder Appwrite-Aufrufe.
  7. Abhängigkeiten des Dienstes (exakt): @anthropic-ai/sdk@0.135.0, node-appwrite@14.1.0 (wie Portal), nodemailer@10.1.2, imapflow@2.5.2, mailparser@3.9.40, playwright-core@1.62.1 (passt zu Chromium im Image). Keine weiteren ohne Eintrag hier.
  8. Modelle: claude-opus-5-5 (Otto, Bob, Bianca, Ben), claude-sonnet-5-5 (Bea, Max, Clara, Theo, Paul), claude-haiku-5-5 (Rita, Kim, Pia, Finn, Ali, Hanna, Sven, Lea, Nora, Arne). Kein temperature/top_p. Strukturierte Antworten über output_config.format (json_schema). Suche und Strukturierung in zwei Aufrufen (Suche mit Werkzeugen → freier Text; danach werkzeugloser Aufruf mit Schema), weil Such-Zitate und output_config.format kollidieren können.
  9. Budget (Kenso): 10 $/Tag, 250 $/Monat in den ersten 4 Wochen, Warnung bei 80 %. Workspace-Ausgabenlimit in der Console zusätzlich. Antwort „HTTP 400 … reached your specified … API usage limits“ oder 429 mit enforced_spend_limit_reached ⇒ sofort budget_stopp.
  10. Mail (Kenso + Spec §8): Kaltmails nie über webklar.com/Hetzner. Höchstens 3 unverlangte Mails/Tag (Erst + Nachfass zusammen), Antworten an Schreibende + Vorschau-Links eigene Grenze 15/Tag, nur werktags 09:00–16:00 Europe/Berlin. Nur Max sendet. Sperrliste vor jedem Versand. Ein SMTP-Login-Fehler ⇒ Versand für den Tag aus (kein Wiederholen; Hetzner-IP-Sperre-Erfahrung gilt sinngemäß).
  11. Keine Umgehung (Kenso): kein Captcha/Cloudflare-Umgehen, kein Login-Scraping, robots.txt beachten (url_not_allowed = auslassen). Plattformen mit Bot-Verbot (Kleinanzeigen, Facebook, Upwork, MyHammer, freelancermap-Login-Bereich, freelance.de-Login-Bereich): nie direkt abrufen; Lesen nur über deren Alarm-Mails im Alarm-Postfach oder Suchmaschinen-Treffer; Senden nur durch Menschen über das Pult.
  12. Texte nach außen: Deutsch, „Sie“ in Kaltmails an Betriebe, „du“ nur, wenn der Anfragende duzt. Kein „Sehr geehrte Damen und Herren“, kein Ausrufezeichen im Betreff. Pflichtbausteine (KI-Hinweis, Quelle, Datenschutz-Link, Widerspruch, Abmelde-Link, Angebotssatz) werden per Code angehängt, nie dem Modell überlassen.
  13. Keine Bot-Leads in WOMS-Kunden: Vor einem bezahlten Deal schreibt kein Teil von Workers in customers, workorders, websiteProjects, customerPortalAccess oder legt Appwrite-Benutzer an.
  14. Zeit: Dienst läuft mit TZ=Europe/Berlin; gespeichert wird ISO-8601 UTC.

3. Besetzung (Rollen-Register) – verbindlich

workerId ist die feste Kennung in Daten, Ereignissen, API und Spiel.

workerId Name Etage (floor) Raum (room) Modell Einzige Rolle Erlaubte Fähigkeiten (caps)
otto Otto bruecke bruecke opus plant die Nacht, verteilt Aufträge, Budget, Morgenbericht plan, report
rita Rita anfragen netz haiku sucht frische „Suche Webdesigner“-Posts über Websuche web_search, web_fetch
kim Kim anfragen kleinanzeigen haiku liest Kleinanzeigen-Suchauftrags-Mails im Alarm-Postfach alert_inbox
pia Pia anfragen portale haiku liest Projekt-Alerts/RSS der Portale alert_inbox, rss
finn Finn anfragen foren haiku sucht in Foren/Frage-Seiten (nur erlaubte) web_search, web_fetch
ali Ali ohne_website aerzte haiku Branchen-Scout Ärzte & Praxen web_search, web_fetch, places, osm
hanna Hanna ohne_website handwerk haiku Branchen-Scout Handwerk wie ali
sven Sven ohne_website selbst haiku Branchen-Scout Selbstständige wie ali
lea Lea ohne_website laden haiku Branchen-Scout Läden & Gastro wie ali
bea Bea bewertung waage sonnet bewertet, verteilt Top 3 / Rest score
max Max gespraeche mail sonnet schreibt Mails und ist einziger Absender draft, send_mail
clara Clara gespraeche forum sonnet schreibt Antworten auf Anfragen draft, pult
nora Nora gespraeche posteingang haiku liest + sortiert Eingänge read_inbox, classify
theo Theo gespraeche abschluss sonnet Nachrichten rund um den Kauf (Entwürfe) draft, pult, offer
bob Bob werkstatt tisch1 opus baut Vorschauen (nur nach Ja) build
bianca Bianca werkstatt tisch2 opus baut Vorschauen build
ben Ben werkstatt tisch3 opus baut Vorschauen build
paul Paul pruefung pruefung sonnet prüft Vorschauen (Screenshots) review, screenshot
arne Arne keller archiv haiku Nie-doppelt-Register + Bot-Lead-Liste register, archive

Etagen von oben nach unten (FLOORS): bruecke, anfragen, ohne_website, bewertung, gespraeche, werkstatt, pruefung, freigabe (Pult, kein Worker), tresor (kein Worker), keller.

Die Tabelle liegt als Daten im Dienst in src/roles.js (ROLES, FLOORS) und als gespiegelte Kopie in WOMS src/lib/workersRoster.js. Ein Test in jedem Repo prüft die Kopie gegen eine gemeinsame Fixture workers-roster.json (gleicher Inhalt in beiden Repos: test/fixtures/workers-roster.json bzw. tests/fixtures/workers-roster.json).

Durchsetzung (P1): assertCap(workerId, cap) wirft CapError, wenn die Rolle die Fähigkeit nicht hat. Jede Fähigkeit hat genau einen Code-Pfad, der sie vor der Ausführung prüft. send_mail existiert nur im Sende-Tor (src/mail/gate.js, P4).


4. Datenmodell (Appwrite, Datenbank woms-database)

Alle Workers-Sammlungen: Rechte [] (nur Server-Schlüssel), documentSecurity=false. WOMS liest nur über die Dienst-API. Angelegt durch scripts/setup-collections.mjs im Dienst (P1; idempotent, --check = nur prüfen, Exit 1 bei Fehlendem). Schema als reine Daten in src/schema.js. Kein Feld heißt data. Dokument-IDs ≤ 36 Zeichen. JSON-Felder sind Strings (…Json). Zeiten: datetime.

Typ-Kürzel: s(n) String max. n, i Integer, f Float, b Boolean, dt Datetime, e[…] Enum, [] Array. * = Pflicht.

4.1 workersSettings (genau ein Dokument, $id = "main")

paused b* · dailyBudgetUsd f* (10) · monthlyBudgetUsd f* (250) · warnPct i* (80) · mailColdPerDay i* (3) · mailReplyCapPerDay i* (15) · sendWindowStart s(5)* ("09:00") · sendWindowEnd s(5)* ("16:00") · sendWeekdaysOnly b* (true) · runsPerNight i* (1) · candidatesPerRun i* (10) · activeRooms s(32)[] (["netz","kleinanzeigen","portale","foren","aerzte","handwerk","selbst","laden"]) · connectedRooms s(32)[] (Räume mit echter Quelle; leer bis eingerichtet) · regions s(64)[] (["Rheinland-Pfalz"]) · previewGate e[erste_n,immer,nie]* ("erste_n") · previewGateN i* (10) · previewsSent i* (0) · offerText s(1000)* (Kensos Satz aus Spec §11.2) · offerProductKey s(32)* ("hosting") · pultStaleDays i* (3) · retentionDays i* (90) · followupDays i* (7) · previewReminderDays i* (5) · previewDeleteDays i* (60) · reportEmail s(256)* (Kensos Adresse) · updatedAt dt · updatedBy s(64)

4.2 workersProspects – Interessenten (Anfragen und Betriebe)

entrance e[anfrage,ohne_website]* · room s(32)* · foundBy s(16)* (workerId) · source s(64)* (z. B. gutefrage, kleinanzeigen-alarm, places) · sourceUrl s(1024) · foundAt dt* · name s(256)* · contactName s(256) · email s(256) · emailSource s(1024) (URL, aus der die Adresse stammt) · phone s(64) · phoneSource s(1024) · website s(512) · street s(256) · postalCode s(10) · city s(128) · placeId s(128) · requestText s(4000) (Wortlaut der Anfrage) · requestedAt dt · score i (0–100) · scoreReason s(2000) · status e[gefunden,bewertet,botlead,kontaktiert,gespraech,ja,vorschau_bau,vorschau_gesendet,angebot,deal,verloren,gesperrt]* · botLeadReason s(500) · threadId s(36) · previewId s(36) · offerId s(36) · customerId s(36) · isHealth b (Heilberuf → keine Telefonnummer speichern) · purgeAfter dt · purgedAt dt · updatedAt dt* Indizes: status, entrance+status, room, foundAt, purgeAfter.

4.3 workersKeys – Nie-doppelt-Register

$id = sha256(kind + ":" + value).hex.slice(0, 32). kind e[dom,mail,tel,pid,ad,namplz]* · prospectId s(36) (leer bei Startbestand) · origin e[workers,seed_alt,seed_kunde]* · blocked b* (true: nie anschreiben) · hint b* (true nur für namplz = kein harter Treffer) · createdAt dt*. value wird nicht gespeichert (nur der Hash), damit die Kürzung nach 90 Tagen nichts übrig lässt.

4.4 workersSuppression – Sperrliste (für immer)

$id = sha256(kind + ":" + value).hex.slice(0, 32). kind e[mail,dom,tel]* · reason e[abmeldung,widerspruch,nein,beschwerde,bounce,kunde,manuell]* · createdAt dt* · source s(64) (workerId oder mensch:<userId>).

4.5 workersSearchLog

workerId s(16)* · room s(32)* · region s(64) · query s(500)* · at dt* · found i* · fresh i* (nach Register-Prüfung neu). Index: room+at.

4.6 workersJobs – Aufträge

kind e[suche,alarm_lesen,bewerten,erstantwort,erstmail,nachfass,einordnen,antwort,bau,pruefung,vorschau_link,angebot,erinnerung,bericht,kuerzen]* · workerId s(16)* · status e[offen,laeuft,warten_batch,fertig,fehler,abgebrochen]* · prospectId s(36) · threadId s(36) · previewId s(36) · attempts i* · batchId s(64) · customId s(64) · payloadJson s(16000) · resultJson s(16000) · error s(2000) · createdAt dt* · startedAt dt · finishedAt dt. Indizes: status, workerId+status.

4.7 workersEvents – Ereignisstrom (Quelle für Spiel, Spur, Kennzahlen)

ts dt* · workerId s(16)* (oder system) · kind s(32)* (Liste §5) · jobId s(36) · prospectId s(36) · text s(300)* (deutscher Satz für Spur/Blase) · link s(1024) · to s(16) (Ziel-workerId bei Übergaben) · tokensIn i · tokensOut i · costUsd f · metaJson s(4000). Index: ts, workerId+ts.

4.8 workersDaily – Tageszähler ($id = "YYYY-MM-DD" Berlin-Datum)

costUsd f* · mailsCold i* · mailsReply i* · searches i* · found i* · contacted i* · deals i* · countersJson s(4000) (pro workerId: {gelesen, gefunden, entwuerfe, gesendet, …}) · warned b* · stopped b*.

4.9 workersThreads – Gesprächsfäden

prospectId s(36)* · channel e[mail,plattform]* · platform s(64) · platformUrl s(1024) · toEmail s(256) · subject s(300) · status e[offen,wartet_auf_kunde,wartet_auf_uns,ja,nein,abgeschlossen,gesperrt]* · lastInboundAt dt · lastOutboundAt dt · followupDueAt dt · followupSent b* · reminderDueAt dt · reminderSent b* · createdAt dt*.

4.10 workersMessages

threadId s(36)* · direction e[in,out]* · author s(64)* (workerId oder mensch:<userId> oder kunde) · subject s(300) · body s(16000)* · messageId s(512) · inReplyTo s(512) · referencesJson s(4000) · sentAt dt · channel e[mail,plattform]* · contactSource s(1024) · classification e[ja,frage,nein,abmelden,abwesend,unzustellbar,beschwerde,unklar] · classificationConfidence f · countsAsCold b* · createdAt dt*.

4.11 workersPult – Freigabe-Pult

kind e[plattform_antwort,vorschau_freigabe,unsicher,beschwerde,preisfrage,mensch_gewuenscht,bau_fehlgeschlagen,mail_antwort]* · prospectId s(36) · threadId s(36) · previewId s(36) · title s(200)* · originalText s(8000) · draft s(8000) · sourceUrl s(1024) · status e[offen,in_arbeit,gesendet,verworfen,veraltet,erledigt]* · claimedBy s(64) · claimedAt dt · doneBy s(64) · doneAt dt · createdAt dt* · staleAt dt. Index: status+createdAt.

tokenHash s(64)* (sha256 hex des 43-Zeichen-base64url-Tokens; Token selbst nie speichern) · prospectId s(36)* · previewId s(36)* · productKey s(32)* ("hosting") · businessName s(256)* · email s(256)* · previewUrl s(1024)* · status e[offen,checkout,bezahlt,abgelaufen,widerrufen]* · expiresAt dt* · stripeSessionId s(128) · customerId s(36) · paidAt dt · createdAt dt*. Index: tokenHash (unique), status. Kauf-URL: https://project.webklar.com/uebernahme.html#t=<token>.

4.13 workersPreviews – Vorschauen

token s(43)* (öffentlicher Pfadteil, base64url, 32 Byte) · prospectId s(36)* · builder s(16)* · status e[bau,pruefung,zurueck,fertig,freigabe,gesendet,verworfen,gescheitert]* · round i* · reviewNotes s(8000) · filesJson s(4000) (Dateiliste) · createdAt dt* · deleteAfter dt* · convertedProjectId s(36) (nach Deal). Öffentliche Adresse: https://entwurf.project.webklar.com/<token>/.


5. Ereignisarten (workersEvents.kind, geschlossene Liste)

kind wer Bedeutung (Spiel-Animation)
auftrag otto Otto vergibt einen Auftrag (to = workerId; Blase)
leerlauf alle nichts zu tun (Kaffee/Schlafen)
gelesen rita, kim, pia, finn, nora Quelle/Postfach gelesen (metaJson.count)
fund Such-Worker neuer Kandidat (Zettel entsteht)
doppelt arne Register-Treffer, Kandidat verworfen (Stempel „schon bekannt“)
uebergabe alle Dokument geht an to (Lauf + Lift)
bewertet bea Punktzahl vergeben
top3 bea Kandidat bekommt Mail-/Antwort-Platz
botlead arne in die Bot-Lead-Liste abgelegt (Keller)
entwurf max, clara, theo Nachricht entworfen
pult clara, theo, nora, paul, system Eintrag ins Freigabe-Pult (Briefkasten-Badge)
gesendet max, system Nachricht verschickt (Max) bzw. Mensch hat gesendet (system, metaJson.by)
eingang nora Antwort eingegangen
sortiert nora eingeordnet (metaJson.classification)
bau_start / bau_fertig bob, bianca, ben Vorschau wird gebaut / ist fertig
pruefung_zurueck / pruefung_ok paul Runde zurück (metaJson.round) / bestanden
vorschau_link max Vorschau-Link verschickt
angebot theo Kauf-Link erzeugt
deal system bezahlt (Pokal + Münzen in den Tresor)
gesperrt system, nora Kontakt auf Sperrliste
budget_warnung / budget_stopp otto 80 % erreicht / Grenze erreicht
pause system Pausenschalter umgelegt (metaJson.paused)
fehler alle Fehler (rotes Ausrufezeichen)

6. Dienst – feste Signaturen (P1 liefert, spätere Pläne nutzen)

Pfade relativ zum Dienst-Repo. Alle Funktionen async, sofern sie I/O machen. deps = injizierte Abhängigkeiten { db, clock, claude, mailer, imap, log }.

Datei Export Signatur / Vertrag
src/config.js loadConfig(env) → { appwrite:{endpoint,projectId,apiKey}, anthropicKey, api:{port:8092}, vorschau:{port:8093, root:'/data/vorschau', baseUrl:'https://entwurf.project.webklar.com'}, smtp:{…}, imap:{…}, alertImap:{…}, reportSmtp:{…}, places:{apiKey}, tz }; wirft mit Liste fehlender Namen (nie Werte).
src/db.js createDb({endpoint,projectId,apiKey}) → { get(col,id), list(col,queries), listAll(col,queries), create(col,data,id?), update(col,id,data), remove(col,id) }; create mit vorhandener $id wirft ConflictError (409).
src/schema.js SCHEMA Array { id, name, attributes:[{key,type,size,required,array,elements,default}], indexes:[…] } exakt nach §4.
src/register/normalize.js normalizeName, normalizePhone, normalizeEmail, companyDomain (Kopie aus WOMS src/lib/leadDuplicates.js, plus Fix „(0)“), normalizeDomain(input), normalizePhoneE164(input), emailKey(input), placeIdKey(input), namePlzKey(name, plz) reine Funktionen; leerer String = kein Schlüssel.
src/register/keys.js keysFor(p) p = Prospect-ähnliches Objekt → [{kind,value}] (ohne leere).
keyId(kind, value) → 32-Zeichen-Hash (§4.3).
claimKeys(deps, keys, {prospectId, origin, blocked=false}) legt alle harten Schlüssel an; → { fresh: boolean, hits: [{kind, prospectId, blocked, origin}], hints: [...] }. fresh=false, sobald ein harter Schlüssel schon existiert (erste Schreiberin gewinnt, 409 = Treffer). namplz erzeugt nur hints.
isSuppressed(deps, {email, domain, phone}) → { suppressed: boolean, reason }.
suppress(deps, {email, domain, phone}, reason, source) idempotent.
src/events.js emit(deps, {workerId, kind, text, jobId?, prospectId?, link?, to?, tokensIn?, tokensOut?, costUsd?, meta?}) schreibt workersEvents; prüft kind gegen §5 und workerId gegen ROLES; kürzt text auf 300.
src/roles.js ROLES, FLOORS, role(workerId), assertCap(workerId, cap), CapError §3.
src/budget.js costOf(model, usage, {batch}) → USD nach Preisliste (Opus 4/20, Sonnet 2/10, Haiku 0,10/0,50 pro MTok; Cache-Lesen ×0,05 Opus/Sonnet, ×0,1 Haiku; Cache-Schreiben 5 min ×1,25, 1 h ×2; Batch Token ×0,5; Websuche 0,01 je Suche, nicht rabattiert).
checkBudget(deps, estUsd) → `{ ok, reason: 'tag'
book(deps, {workerId, usd, counters}) addiert in workersDaily; löst einmal budget_warnung bzw. budget_stopp aus.
src/claude.js createClaude({apiKey, sdk}) → { ask(opts), search(opts), batch }.
ask(deps, {workerId, model?, system, input, schema?, images?, maxTokens, effort?}) prüft assertCap nur für werkzeuglose Aufrufe nicht; prüft checkBudget; ruft Messages API; bucht Kosten; → `{ data
search(deps, {workerId, system, task, maxUses, allowedDomains?, blockedDomains?}) assertCap(workerId,'web_search'); web_search_20260318 + web_fetch_20260318 mit allowed_callers:['direct']; folgt pause_turn max. 5×; → { text, sources:[{url,title}], usage, costUsd }.
src/settings.js getSettings(deps), updateSettings(deps, patch, by) Standardwerte nach §4.1; updateSettings validiert Grenzen (Budget > 0, mailColdPerDay ≤ 3 ohne Ausnahme).
src/schedule.js berlinNow(clock), isDue(kind, now, lastRunDay), inSendWindow(now, settings) Muster Serverpuls (src/schedule.js dort).
src/jobs.js enqueue(deps, {kind, workerId, prospectId?, threadId?, previewId?, payload}), next(deps, {kinds}), finish(deps, jobId, result), fail(deps, jobId, error)
src/pult.js (P3) addPult(deps, {kind, title, prospectId?, threadId?, previewId?, originalText?, draft?, sourceUrl?}), claimPult, markSent, discard, expireStale
src/mail/gate.js (P4) sendMail(deps, {workerId:'max', prospectId, threadId, to, subject, text, cold: boolean, inReplyTo?, references?}) einziger Versandweg: assertCap('max','send_mail') → Sperrliste → Tageszähler/Grenzen → Sendefenster → Pflichtbausteine → SMTP → workersMessages + Ereignis gesendet. → { sent:boolean, reason?, messageId? }.
src/mail/footer.js (P4/P6) appendCompliance(text, {prospect, cold, unsubscribeUrl}) hängt Pflichtbausteine an (Spec §8.4).
src/vorschau/server.js (P4) startVorschauServer(deps, {port, root}) statisch, nur GET /<token>/…, Header X-Robots-Tag: noindex, nofollow, kein Verzeichnis-Listing, 404 bei unbekanntem Token/abgelaufen. Zusätzlich Abmelden: GET /abmelden/<u> (Seite mit Knopf) und POST /abmelden/<u> (RFC 8058 One-Click, Body List-Unsubscribe=One-Click) → suppress(...,'abmeldung') + Ereignis gesperrt. <u> = unsubToken(prospectId) aus src/mail/unsub.js (HMAC-SHA256 mit WORKERS_UNSUB_SECRET, base64url, 22 Zeichen + . + prospectId).

6.1 Prozess und Tick (P1)

src/main.js: Konfiguration laden → db → API-Server (8092) → (P4) Vorschau-Server (8093) → Tick alle 60 s. Der Tick fragt isDue für: nachtplan (22:00), nacht2 (01:00), bericht (06:30), kuerzen (03:00, P6), pultAlt (07:00, P3), posteingang (alle 2 min, P4), alarme (alle 10 min, P3), senden (alle 5 min im Sendefenster, P4), batches (alle 5 min). Tagesmarker in workersDaily.countersJson.__runs (neustartfest). Bei paused arbeitet nur pause-sichere Arbeit (Posteingang lesen + Sperrliste), nichts wird gesendet oder gebaut.

6.2 HTTP-API des Dienstes (P1 Gerüst, spätere Pläne ergänzen)

Basis intern: http://webklar-workers:8092. Von WOMS über nginx https://ticket.webklar.com/api/workers/… (neuer location-Block nach Muster /api/serverpuls/). Auth: Header X-Appwrite-JWT → Appwrite GET /account (60 s Cache) → Label mitarbeiter oder admin nötig; admin für Einstellungen. Antworten JSON, Fehler { error: '<deutscher Satz>' }.

Methode + Pfad Plan Recht Antwort
GET /api/workers/health P1 keins { ok:true, version }
GET /api/workers/state P1 Team `{ paused, workers:[{workerId, status:'arbeitet'
GET /api/workers/events?after=<ISO>&limit=<≤200> P1 Team { events:[{id, ts, workerId, kind, text, link, to, prospectId, costUsd, meta}] } aufsteigend nach ts
GET /api/workers/worker/:workerId/trace?limit=<≤50> P1 Team { worker:{…ROLES-Eintrag, model}, today:{counters, tokens, costUsd}, events:[…] }
POST /api/workers/pause {paused:boolean} P1 Team { paused } (+ Ereignis pause)
GET /api/workers/settings / PUT /api/workers/settings P1 Admin workersSettings ohne interne Zähler
GET /api/workers/pult?status=offen P3 Team { items:[…§4.11] }
POST /api/workers/pult/:id/claim · /sent · /discard · /done P3 Team Eintrag
POST /api/workers/pult/:id/reply {text} P4 Team sendet über sendMail als mensch:<userId> (Mail-Fäden)
POST /api/workers/pult/:id/approve-preview P4 Team gibt Vorschau-Link frei
GET /api/workers/botleads?room=&q=&page= P6 Team { items:[…], total } (Telefon nur wenn Quelle vorhanden und nicht isHealth)
GET /api/workers/prospect/:id P3 Team Interessent + Faden + Nachrichten + Vorschau

7. WOMS – feste Namen (P2 liefert)

Datei Export / Zweck
src/lib/navLinks.js Eintrag { key:'workers', label:'Workers', href:'/workers' } ersetzt leads (nicht mehr paused).
src/App.jsx Route /workers → React.lazy(() => import('./pages/WorkersPage.jsx')); /leads leitet auf /workers um.
src/lib/workersApi.js createWorkersApi({ getJwt, fetchImpl }) → { state(), events(after), trace(workerId), pause(paused), settings(), saveSettings(patch), pult(status), pultAction(id, action, body?), botleads(params), prospect(id) }; Header X-Appwrite-JWT; Basis /api/workers.
src/lib/workersRoster.js ROLES, FLOORS (Spiegel von §3), hatForModel(model).
src/lib/workersView.js reine Ableitungen: formatUsd, formatCount, workerStatusLabel, kpiTiles(kpis).
src/lib/workersEvents.js reduceEvents(stateByWorker, events) → { [workerId]: { status, task, lastTs } }; eventToSteps(event) → Animationsschritte für die Regie.
src/hooks/useWorkersFeed.js Polling: state alle 15 s, events?after= alle 5 s, Pause bei document.visibilityState==='hidden'.
src/components/workers/engine/* scene.js: mountTower(canvas, { onSelect }) → { destroy(), setState(state), pushEvents(events), select(workerId), setPaused(bool) } + sprites.js, props.js, layout.js, building.js, lift.js, director.js, fx.js (Plain Canvas 2D, Vorlage scene-turm.js).
src/components/workers/{WorkersStage,WorkerCard,WorkersKpis,WorkersSettingsDialog}.jsx UI-Teile. P3 ergänzt PultList.jsx, P6 BotLeadList.jsx.
src/pages/WorkersPage.jsx Reiter Hochhaus · Freigabe (P3) · Bot-Leads (P6).
src/styles/workers.css Klassen mit Präfix wk-. Schrift „Pixelify Sans“ selbst gehostet (public/fonts/PixelifySans-*.woff2).
tests/fixtures/workers-roster.json gemeinsame Fixture (§3).

8. Portal – feste Namen (P5 liefert)

Datei Zweck
public/uebernahme.html + public/js/uebernahme.js Seite „Website übernehmen“: zeigt Betrieb, Vorschau-Link, Angebotssatz, Abo-Bedingungen v5 (Wortlaut wie normaler Checkout), Widerruf/Leistungsbeginn, Kundenart, Rechnungsangaben; Token aus location.hash.
server/routes/uebernahme.js GET /api/uebernahme/:token → Angebotsansicht oder 404; POST /api/uebernahme/:token/checkout → Stripe-Checkout-URL. Öffentlich, X-Robots-Tag: noindex, createWriteLimiter, 404 bei jedem ungültigen/abgelaufenen Token (Muster routes/auftrag.js).
server/services/workersKauf.js findOfferByToken(token), createWorkersCheckout(offer, consent, buyer), handleWorkersCheckoutEvent(event), isWorkersCheckoutEvent(event) (Session-metadata.workersOfferId vorhanden).
server/routes/abo.js im Webhook vor dem normalen Zweig: if (isWorkersCheckoutEvent(event)) return handleWorkersCheckoutEvent(event).
server/mail-templates/K44 Website übernommen.html Zugangsdaten + Begrüßung (nächste freie Nummer laut Code-Landkarte; vor dem Anlegen erneut alle Branches prüfen).
server/config.js collections.workersOffers, collections.workersProspects, collections.workersPreviews, collections.workersEvents (Env APPWRITE_COLLECTION_WORKERS_*, Standard = Sammlungs-ID).

Ablauf nach Zahlung (handleWorkersCheckoutEvent, idempotent über workersOffers.status): Kunde mit createCustomerWithPortalAccess({ name, email, password:<zufällig>, kundenart }) anlegen (Status danach customer) → Stripe-Customer + Subscription-Metadaten webklarCustomerId setzen → syncSubscription → recordAbschluss → Nachweis (featureConsents) → K44 senden → forgetStoredPortalPassword → T02 an Team → Vorschau als Kundenprojekt übernehmen (Repo aus Vorlage in Org WEBklar mit Systembenutzer, Dateien hochladen, websiteProjects mit customerId, Typ preview) → workersOffers.status='bezahlt', workersProspects.status='deal' + customerId → Ereignis deal (workerId system).


9. Betrieb – feste Namen

Was Wert
Container webklar-workers (Compose-Dienst workers, build: ./workers, Image webklar-workers:latest, Muster Serverpuls-Dockerfile + aircard-api-Härtung)
Ressourcen mem_limit: 768m, memswap_limit: 768m, cpus: 1.0, NODE_OPTIONS=--max-old-space-size=384, init: true, restart: unless-stopped, user: node, read_only: true, tmpfs: /tmp, cap_drop: [ALL], security_opt: [no-new-privileges:true], logging json-file max-size 5m max-file 2, TZ=Europe/Berlin
Volume workers_data:/data (Vorschauen unter /data/vorschau/<token>/)
Netz webklar-network; intern http://webklar-appwrite/v1 für Appwrite
Chromium im Image per apk add --no-cache chromium (für Paul), Screenshots seriell, 60 s Zeitlimit
nginx (WOMS) neuer Block location /api/workers/ → webklar-workers:8092 in /home/webklar/apps/ticket.webklar.com/nginx/ticket.webklar.com.conf (Backup + nginx -s reload im Container webklar-ticket)
Traefik Datei /home/webklar/apps/traefik/dynamic/workers-entwurf.yml: Router Host(\entwurf.project.webklar.com`), Priorität 200, certResolver: letsencrypt, Ziel http://webklar-workers:8093` (P4)
Reservierte Subdomain entwurf im Portal-Subdomain-Schutz sperren (P5)
Env (.env → Compose) WORKERS_APPWRITE_API_KEY→APPWRITE_API_KEY, WORKERS_ANTHROPIC_API_KEY→ANTHROPIC_API_KEY, APPWRITE_ENDPOINT=http://webklar-appwrite/v1, APPWRITE_PROJECT_ID=6a1058610003c5a13a05, WORKERS_SMTP_HOST/PORT/USER/PASS/FROM (Absender-Postfach der neuen Domain), WORKERS_IMAP_HOST/PORT/USER/PASS (gleiches Postfach), WORKERS_ALERT_IMAP_HOST/PORT/USER/PASS (Alarm-Postfach), WORKERS_REPORT_SMTP_* (interner Morgenbericht über das bestehende webklar.com-Konto erlaubt, da kein Kaltkontakt), WORKERS_PLACES_API_KEY (P6), WORKERS_UNSUB_BASE=https://entwurf.project.webklar.com/abmelden, WORKERS_UNSUB_SECRET (32 Byte zufällig)
Serverpuls Gruppe {"app":"Workers","match":["webklar-workers*"],"folders":["workers"]} in apps/serverpuls/config/apps.json
Anthropic eigener Console-Workspace „Workers“, Ausgabenlimit 250 $/Monat (Kenso)

10. Was Kenso vorab einrichtet (Spec §12)

Console-Workspace + Schlüssel; Domain webklar-team.de (INWX) + Google-Workspace-Postfächer (Absender + Alarme) oder Alternative; Konten + Suchaufträge auf Kleinanzeigen/freelancermap/freelance.de an das Alarm-Postfach; Freigabe des Datenschutz-Abschnitts „Interessenten“. Jeder Plan nennt am Ende seine Betriebsanleitung (Schritte, die Kenso ausführt: Skripte, .env, Compose, nginx, Push).