Workers: Plan-Übersicht und gemeinsame Schnittstellen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
775087a8f3
commit
dbfbf36b08
1 file changed
+274
@@ -0,0 +1,274 @@
|
|||||||
|
# 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`.
|
||||||
|
|
||||||
|
### 4.12 `workersOffers` – Kauf-Links (Schnittstelle zum Portal, P4 schreibt, P5 liest/aktualisiert)
|
||||||
|
`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'|'monat'|'pause'|null, warn: boolean }`. |
|
||||||
|
| | `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|text, usage, costUsd }`. Wirft `BudgetStop` bei Budget-Antworten der API (§2.9). |
|
||||||
|
| | `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'|'wartet'|'leerlauf'|'grau'|'fehler', task, counters, connected}], kpis:{newAbosMonth, mrrNewEur, dealsMonth, openThreads, pultOpen, botLeads, apiTodayUsd, apiMonthUsd, dailyBudgetUsd, monthlyBudgetUsd}, lastEventTs }` |
|
||||||
|
| `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).
|
||||||
Reference in new issue
Block a user