diff --git a/docs/superpowers/plans/2026-10-17-workers-00-uebersicht.md b/docs/superpowers/plans/2026-10-17-workers-00-uebersicht.md index 4b4ce7e..a073b72 100644 --- a/docs/superpowers/plans/2026-10-17-workers-00-uebersicht.md +++ b/docs/superpowers/plans/2026-10-17-workers-00-uebersicht.md @@ -207,7 +207,7 @@ Pfade relativ zum Dienst-Repo. Alle Funktionen `async`, sofern sie I/O machen. ` | `src/schedule.js` | `berlinNow(clock)`, `runKey(kind, now)`, `isDue(kind, now, last)`, `inSendWindow(now, settings)` | `now` = Ergebnis von `berlinNow(clock)`; Tick-Arten in §6.1; Muster Serverpuls. | | `src/priority.js` (H1) | `rankOf(kind)`, `JOB_RANKS` | Rang 0–5 für jede Job- und Tick-Art (§12 H1); unbekannte Art wirft. | | `src/jobs.js` | `enqueue(deps, {kind, workerId, prospectId?, threadId?, previewId?, payload}) → Job`, `next(deps, {kinds, workerIds?}) → Job oder null`, `finish(deps, jobId, result)`, `fail(deps, jobId, error, {retry=true})`, `reclaimStale(deps, {olderThanMs})` | `next` fragt die angeforderten Arten in `rankOf`-Reihenfolge ab und liefert je Art den ältesten `offen`-Job (H1); Job-Objekt §4.14; nach 3 Versuchen `fehler`. | -| `src/pult.js` (P3; P4 erweitert) | `addPult(deps, {kind, title, workerId?, prospectId?, threadId?, previewId?, originalText?, draft?, sourceUrl?, slot?}) → Dokument + created`, `pultReplyId(prospectId, slot?)`, `claimPult`, `markSent`, `discard`, `donePult`, `expireStale` | `workerId` = Autor-Worker (Standard: `by` der Pult-Art; Antwort-Arten prüfen `assertCap(workerId,'pult')`, Theos Kauf-Link-Eintrag kommt also mit `workerId:'theo'`); `created:false` = Eintrag bestand schon. | +| `src/pult.js` (P3; P4 erweitert) | `addPult(deps, {kind, title, workerId?, prospectId?, threadId?, previewId?, originalText?, draft?, sourceUrl?, slot?, idKey?}) → Dokument + created`, `pultReplyId(prospectId, slot?)`, `claimPult`, `markSent`, `discard`, `donePult`, `expireStale` | `workerId` = Autor-Worker (Standard: `by` der Pult-Art; Antwort-Arten prüfen `assertCap(workerId,'pult')`, Theos Kauf-Link-Eintrag kommt also mit `workerId:'theo'`); `created:false` = Eintrag bestand schon. `slot` nur bei `plattform_antwort`/`mail_antwort` (sonst 400); `idKey` für Arten ohne Antwort: Id = `detId('pult', idKey)`, eine Wiederholung liefert den bestehenden Eintrag (gleich welcher Status). | | `src/mail/gate.js` (P4) | `sendMail(deps, {workerId:'max', prospectId, threadId?, to, subject, text, cold: boolean, kind?, author?, inReplyTo?, references?, idempotencyKey?})` | **einziger** Versandweg. `kind` ∈ erst, nachfass, antwort, vorschau_link, erinnerung (Standard `cold ? 'erst' : 'antwort'`); `author` = `'max'` (Standard), `'clara'`, `'theo'` oder `'mensch:'` (überspringt nur das Sendefenster und ersetzt den KI-Hinweis); `idempotencyKey`: höchstens ein Versand je Schlüssel. Prüfkette, erster Verstoß = `reason`: `assertCap('max','send_mail')` → Pause (`pause`) → Notbremse (`versand_gestoppt`, H4) → Empfänger = `thread.toEmail` und, falls gesetzt, `prospect.email` (`adresse_ungueltig`, H3) → Inhalt nach `src/mail/rules.js` (H3: `platzhalter`, `verbotene_phrase`, `betreff_ausrufezeichen`, `text_zu_kurz`, `text_zu_lang`, `link_anzahl`, `link_ziel`) → Sperrliste (`gesperrt`) → SMTP-Tagessperre (`smtp_gesperrt`) → Zusatzprüfungen (`registerGateCheck`, P6 `coldChecks`: `datenschutz`, `adresse_nicht_belegt`, `kein_kaltkontakt`, `kaltmail_ohne_antwort`, `status_unpassend`) → Datenschutz-Schalter bei der ersten Nachricht im Faden (`datenschutz`) → Tageszähler (`tageslimit`, `antwortlimit`) → Sendefenster (`fenster`) → Pflichtbausteine (`quelle_fehlt`) → Reservierung (`doppelt`) → SMTP (`smtp_auth`, `abgelehnt`, `smtp_fehler`, `nicht_konfiguriert`) → `workersMessages` + Ereignis `gesendet`. → `{ sent:boolean, reason?, messageId? }`. Jeder Verstoß ist eine Verweigerung mit Grund, nie eine stille Korrektur. `registerGateCheck(async (deps, ctx) => reason oder null)` hängt weitere Prüfungen ein. | | `src/mail/footer.js` (P4/P6) | `appendCompliance(text, {prospect, cold, unsubscribeUrl, first?, human?, offerText?})` | hängt Pflichtbausteine an (Spec §8.4). `cold:true` → `buildColdFooter` (`src/mail/coldFooter.js`, P6; dessen Wortlaut ist maßgeblich, P4 liefert bis dahin einen gleichwertigen Platzhalter-Zweig), `cold:false` → kurzer Abmelde-Block. Der Block wird erst **nach** den Inhaltsprüfungen angehängt und zählt dort nicht mit. | | `src/mail/links.js` (P3) | `DATENSCHUTZ_URL = 'https://www.webklar.com/datenschutz#interessenten'`, `IMPRESSUM_URL = 'https://www.webklar.com/impressum'`, `WEBKLAR_URL = 'https://www.webklar.com'` | einzige Quelle der festen Adressen in Mails (P3 Clara, P4 Footer, P6 Kaltmail). | @@ -310,7 +310,7 @@ Ablauf nach Zahlung (`handleWorkersCheckoutEvent`, unter einer Sperre je Angebot | 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` (ab P4 `/tmp:size=256m`, dazu `HOME=/tmp`, `XDG_CACHE_HOME=/tmp/.cache` für Chromium), `cap_drop: [ALL]`, `security_opt: [no-new-privileges:true]`, `logging json-file max-size 5m max-file 2`, `TZ=Europe/Berlin` | +| Ressourcen | `mem_limit: 768m`, `memswap_limit: 768m` (ab P4 `1g`/`1g` wegen Chromium), `cpus: 1.0`, `NODE_OPTIONS=--max-old-space-size=384`, `init: true`, `restart: unless-stopped`, `user: node`, `read_only: true`, `tmpfs: /tmp` (ab P4 `/tmp:size=256m`, dazu `HOME=/tmp`, `XDG_CACHE_HOME=/tmp/.cache` für Chromium), `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//`, Lesestand `/data/imap-state.json`); das Volume steht unter dem Top-Level-`volumes:` der Compose-Datei. Zusätzlich (H5) `./workers/prompts:/app/prompts:ro`: Prompt-Änderungen wirken nach `git pull` im Produktions-Checkout ohne Neubau (Neuladen bei geänderter mtime). | | Portal (P5) | Der Portal-Container `kundenbereich-server` hängt `workers_data:/workers-vorschau:ro` (lesend) an und bekommt `WORKERS_KAUF_ENABLED=${WORKERS_KAUF_ENABLED:-off}` und `WORKERS_VORSCHAU_ROOT=/workers-vorschau/vorschau`; Verzeichnisse im Volume sind 0755, Dateien 0644 (P4), damit das Portal sie lesen kann. | | Netz | `webklar-network`; intern `http://webklar-appwrite/v1` für Appwrite | @@ -372,7 +372,7 @@ Stand: Abgleich der Teilpläne P1–P6 am 17.10.2026. Die Teilpläne haben ihre | P4-E4 | P4 | Message-ID ``; Zuordnung über `In-Reply-To`/`References` gegen gespeicherte ausgehende Nachrichten; eingehende Dokument-ID `sha256('in:'+messageId)`. | | P4-E5 | P4 | `src/gespraeche/index.js` liefert `tickJobs` (`posteingang`, `senden`, `vorschauAufraeumen`) und `routes`, **keine** `jobRunners`; Mail-Aufträge (`antwort` von `max`) nur im Tick `senden`, die Werkstatt läuft im Hintergrund und holt zusätzlich `antwort`-Aufträge von `clara` (Plattform-Folge-Entwurf, P4-E12); Einhängen über `buildTickJobs()`/`buildRoutes()` (A4). | | P4-E6 | P4 | `loadConfig` liefert `unsub`, `vorschau.chromiumPath`, `autoMail`. | -| P4-E7 | P4 | `addPult` mit `slot` (`vorschau`, `angebot`, ab P4-T34 auch `folge<8 Hex>`); `markSent` setzt den Interessenten nur aus `gefunden`/`bewertet` auf `kontaktiert`; `vorschau_freigabe` nur für Mail-Fäden. | +| P4-E7 | P4 | `addPult` mit `slot` (`vorschau`, `angebot`, ab P4-T34 auch `folge<8 Hex>`); `markSent` setzt den Interessenten nur aus `gefunden`/`bewertet` auf `kontaktiert`; `vorschau_freigabe` nur für Mail-Fäden; Option `idKey` (z. B. `:freigabe`, `:bau_fehlgeschlagen`) für feste Ids bei Arten ohne Antwort. | | P4-E8 | P4 | Index `workersPreviews.idx_status`; `workersThreads.idx_status` bleibt bei P1 (nicht doppelt). | | P4-E9 | P4 | Tick `vorschauAufraeumen` täglich ab 03:20. | | P4-E10 | P4 | Gesperrte Interessenten ⇒ offene Angebote `abgelaufen`; Vorschau-Server 404 bei `convertedProjectId`; P4 legt nur Angebote mit `hosting` an. | diff --git a/docs/superpowers/plans/2026-10-17-workers-p4-gespraeche-werkstatt.md b/docs/superpowers/plans/2026-10-17-workers-p4-gespraeche-werkstatt.md index 724815f..89ee884 100644 --- a/docs/superpowers/plans/2026-10-17-workers-p4-gespraeche-werkstatt.md +++ b/docs/superpowers/plans/2026-10-17-workers-p4-gespraeche-werkstatt.md @@ -57,7 +57,7 @@ - Empfängeradresse im Auftrag weicht vom Faden (oder vom Interessenten) ab, Kaltmail hat zwei Links oder einen fremden Link statt genau einem (`WEBKLAR_URL`), Entwurf nennt einen Preis/Rabatt/Link, der nicht erlaubt ist → nichts wird gesendet (`adresse_ungueltig`, `link_anzahl`, `link_ziel`, Entwurf verworfen). Pin: T04a, T05, T07, T08, T16. - Platzhalterrest (`{{`, `TODO`, ``), verbotene Phrase, `!` im Betreff, Kaltmail außerhalb 60–180 Wörter oder Antwort über 300 Wörter → nichts wird gesendet, der Auftrag endet `skipped` und im Pult liegt ein Eintrag `unsicher` mit dem Entwurf; ein Mensch aus dem Pult wird nur auf Platzhalter geprüft. Pin: T04a, T07, T16, T29. - Zwei harte Bounces unter den letzten 10 Mails oder 7-Tage-Quote über 5 % (Nenner mindestens 20) → `sendingHalted`, genau ein Ereignis und eine Mail, das Tor verweigert alles mit `versand_gestoppt` (auch `mensch:`), alle Aufträge bleiben offen ohne Versuchsverbrauch; weicher Bounce und Abwesenheit zählen nicht; nur ein Admin hebt auf. Pin: T14, T14a, T07, T30. -- Kundenantwort von einer Plattform (im Pult eingefügt oder als passende Plattform-Benachrichtigung im Alarm-Postfach) → genau eine eingegangene Nachricht und ein Auftrag `einordnen`, auch bei Doppelklick, gleicher Mail oder Absturz mitten im Anlegen; „abmelden“ und Abmahnung wirken auch hier sofort ohne Modell und bei Pause; eine Frage erzeugt einen Entwurf von Clara im Pult (`plattform_antwort`, Slot `folge<8 Hex>`) und nie eine Mail von Max; ein Suchauftrags-Sammelalarm, eine unechte Mail oder ein Mail-Faden wird nie als Plattform-Antwort gewertet; ohne eingetragenen Betreff-Ausdruck (`REPLY_SUBJECTS`) bleibt der Mail-Weg aus und nur das Einfügen aktiv. Pin: T34, T14, T15. +- Kundenantwort von einer Plattform (im Pult eingefügt oder als passende Plattform-Benachrichtigung im Alarm-Postfach) → genau eine eingegangene Nachricht und ein Auftrag `einordnen`, auch bei Doppelklick, gleicher Mail oder Absturz mitten im Anlegen; „abmelden“ und Abmahnung wirken beim Einfügen sofort ohne Modell und bei Pause, bei einer Plattform-Benachrichtigung aus dem Alarm-Postfach entscheidet Nora (feste Regeln greifen dort nicht, weil die Fußzeilen der Plattform sonst jeden Kontakt sperren würden); eine Frage erzeugt einen Entwurf von Clara im Pult (`plattform_antwort`, Slot `folge<8 Hex>`) und nie eine Mail von Max; ein Suchauftrags-Sammelalarm, eine unechte Mail oder ein Mail-Faden wird nie als Plattform-Antwort gewertet; ohne eingetragenen Betreff-Ausdruck (`REPLY_SUBJECTS`) bleibt der Mail-Weg aus und nur das Einfügen aktiv. Pin: T34, T14, T15. ## File Structure