Akquise Pipeline

3-stufige Email-Kampagne mit Web-Approval, MongoDB-Draft & Seq-Logging — Stand 2026-04-20

Beschreibung

Die Akquise Pipeline wandelt fertige Websites in Kunden um. Der 🤖 S6a-Bot erstellt das Akquise-Ticket (Screenshot, Angebotsseite aktualisieren, CUSTOMER_REF deployen). Der 🤖 S6b-Bot verwaltet die 3-stufige Mail-Kaskade Email-1 (Erstansprache) → Email-2 (Follow-up nach 7 Tagen) → Email-3 (Letzte Chance nach weiteren 7 Tagen).

Ab v2 (2026-04-20) läuft die Freigabe nicht mehr über Jira-Labels. Michael bekommt pro Stufe eine Vorschau-Mail mit 4 klickbaren Buttons — der Server (minicon-website-service) speichert die Entscheidung in einem AkquiseEmailDraft-Dokument (MongoDB), der Cron pollt diesen Draft und handelt entsprechend. Jeder Übergang geht als CLEF-Event an logging.minicon.eu.

⏰ S6a: 0 6,12,18 * * * ⏰ S6b: 0 9,15,21 * * * 🤖 OpenClaw-Cron 👤 1 Approver: Michael 📧 3 Email-Stufen 📊 Seq-Logging live
Zwei-Phasen-Flow pro Stufe

Phase A — Preview: S6b rendert Mail → POST /api/s6b/draft → Server gibt Token + 4 Approval-URLs zurück → Vorschau-Mail mit Button-Header an Michael → Jira-Label akquise-email-{N}-preview-sent.

Phase B — Dispatch: S6b pollt GET /api/s6b/draft/{token} → verzweigt nach decision: approved → Resend-Versand + Label akquise-email-{N}; rejected → Label akquise-pause + Jira-Kommentar mit Grund; regenerate → Feedback-Kommentar [S6b-Feedback] ins Ticket, Preview-Label zurücksetzen (Phase A rendert beim nächsten Run neu).

Die gleichen zwei Phasen gelten identisch für Email-1, Email-2 und Email-3 — nur JQL-Trigger und Template unterscheiden sich.

Phase 1 — S6a Akquise-Vorbereitung
1

S6a – Akquise-Ticket anlegen

Cron ID: 6ab77edf-2738-4190-acbc-b03a7e10b2d0

Schedule: 0 6,12,18 * * * (3× täglich)

Agent: 🤖 akquise

Trigger: Build-Ticket mit po-approved + build-ticket

  1. JQL: project=DAHN AND labels="build-ticket" AND labels="po-approved"
  2. Dreifach-Duplikat-Check pro Ticket (akquise-ticket / discovery-done > 1 / bereits vorbereitet)
  3. Screenshot für Jira: node C:\working\atlas\scripts\screenshot.js https://{slug}.minicon.eu → {slug}.jpg
  4. Akquise-Ticket in Jira anlegen:
    • Summary: [Akquise] {Firmenname}
    • Labels: akquise-ticket, siteid-{slug}, akquise-bereit
    • Link relates to Build-Ticket
    • Screenshot als Attachment
  5. CUSTOMER_REF in website-webseiten/src/app/page.tsx eintragen (2 Keys) + commit + push → GitHub-Action deployed auf Cloudflare Pages
  6. Label-Transition: akquise-bereitakquise-vorbereitet

ℹ️ Screenshot-Rendering auf Angebotsseite: Die Kunden-Referenzen auf webseiten.minicon.eu/?ref={slug} nutzen seit 2026-04-19 thum.io (https://image.thum.io/get/width/1200/noanimate/…) statt statischer /previews/{slug}.jpg. Immer aktuell, keine Pflege. Datenschutz-Seite: webseiten.minicon.eu/datenschutz.

Ergebnis: Akquise-Ticket (Label akquise-vorbereitet) + aktualisierte Angebotsseite mit personalisiertem Ref-Link

🤖 S6a Bot
Phase 2 — S6b Email-Kaskade mit Approval-Flow

Cron ID: f7a72879-eef9-4af3-b1c5-df6b95753d14 · Schedule: 0 9,15,21 * * * · Timeout: 900s

Pro Run: Bot durchläuft 2 Phasen × 3 Stufen → Max. 5 Tickets je Phase (Rate-Limit). Ältestes zuerst (ORDER BY updated ASC).

2

Phase A – Vorschau rendern & Draft anlegen

JQL (Stufe 1):

project=DAHN AND labels="akquise-ticket" AND labels="akquise-vorbereitet" AND labels NOT IN (akquise-email-*-preview-sent, akquise-email-1, akquise-kontakt, akquise-pause, akquise-manuell-verkauft, akquise-selbst)

JQL (Stufe 2): … labels="akquise-email-1" AND updated < -7d

JQL (Stufe 3): … labels="akquise-email-2" AND updated < -7d

Aktionen:

  1. Pre-Send-Check (nur Stufe 1): pre-send-check.ps1 -SiteId {slug} — Non-200 → SKIP + Seq-Event s6b.preview.skipped
  2. Hub: GET api.minicon.eu/api/admin/customers/{siteId}customer.email, botPersona.name, company, contacts[0].name
  3. Features aus Build-Ticket (S1d/S1f/S1g-Kommentare) sammeln — individuell pro Firma
  4. Template rendern: email-{N}-{typ}.html mit Platzhaltern {Anrede} (ohne trailing Komma!), {firmenname}, {preview_url}, {angebots_url}, {bot_name}, {features}
  5. Preview-URL Locale-Fix: HEAD-Request auf /de/ — falls 200, Pfad erweitern (für mehrsprachige Sites wie Felsland)
  6. Feedback einarbeiten: Falls [S6b-Feedback]-Kommentar seit letzter Preview → Tonfall/Features anpassen, Kommentar quittieren
  7. Draft anlegen:
    POST/api/s6b/draft
    Header: X-Internal-Secret: $SECRET_HUB_INTERNAL_SECRET
    Body: {siteId, ticketKey, stage, custEmail, custName, botName, subject, html, correlationId}
    Response: {token, approve, reject, edit, regenerate, expiresAt}
  8. Vorschau-Mail an Michael (Resend, From: noreply@support.minicon.eu, To: michael.nikolaus@minicon.eu):
    ✅ Freigeben ❌ Abbrechen ✏️ Editieren 🔁 Neu generieren
    Links zeigen auf api.minicon.eu/api/s6b/admin-approve?token=…&action=…
  9. HTML als Jira-Attachment email-{N}-{siteid}.html
  10. Label: akquise-email-{N}-preview-sent
  11. Seq: s6b.preview.rendered, s6b.preview.sent (correlationId, siteId, ticketKey, stage)
🤖 S6b Bot
3

Michael entscheidet (Web-UI)

Trigger: Michael erhält Vorschau-Mail, klickt Button

Token-TTL: 7 Tage

Aktionen:

  1. ✅ Freigeben → GET → direktes Approve → decision=approved. Bestätigungsseite. Nächster Cron-Run versendet.
  2. ❌ Abbrechen → GET zeigt Formular mit Begründungs-Textarea → POST → decision=rejected, rejectReason gespeichert.
  3. ✏️ Editieren → GET zeigt TinyMCE-Editor (CDN-Version, no-api-key) mit 3 Tabs:
    • WYSIWYG
    • Raw HTML
    • Preview (iframe srcdoc)
    POST mit neuem html → Draft updated. Anschließend Approve/Reject/Regenerate möglich.
  4. 🔁 Neu generieren → GET zeigt Feedback-Textarea → POST → decision=regenerate, regenerateFeedback gespeichert. Cron wandelt in Jira-Kommentar [S6b-Feedback] …, entfernt Preview-Label → Ticket fällt zurück in Phase A.

Seq: s6b.admin.decision (decision, approver, feedbackLength / reasonLength) · s6b.draft.edited bei Edit

Approver: michael.nikolaus@minicon.eu (hardcoded im Server-Config)

👤 Michael 🖥️ Server
4

Phase B – Cron dispatcht Entscheidung

JQL: labels="akquise-email-*-preview-sent" AND labels NOT IN (akquise-kontakt, akquise-manuell-verkauft, akquise-selbst, akquise-pause)

Pro Ticket:

  1. Token aus letztem [S6b]-Kommentar extrahieren
  2. Draft-Status holen:
    GET/api/s6b/draft/{token}
  3. Verzweigen nach decision:
    • approved → Resend an draft.custEmail (CC Michael) · POST /api/s6b/draft/{token}/mark-sent · Label akquise-email-{N} + Preview-Label entfernen · Seq s6b.customer.sent
    • rejected → Label akquise-pause · Jira-Kommentar mit rejectReason · Seq s6b.cascade.ended mit endReason=rejected
    • regenerate → Jira-Kommentar [S6b-Feedback] … · Preview-Label entfernen (Ticket zurück in Phase A) · Seq s6b.admin.regenerate
    • pending → SKIP (wartet weiter auf Michael) · Seq Debug s6b.dispatch.pending
    • sent → Race-Handling: Labels aufräumen, kein Doppelversand
  4. Bei Resend-Fail (≠ 200) → Seq s6b.customer.failed · KEIN Label-Update (Cron retry beim nächsten Run)
  5. Vor Versand: curl -I {preview_url} — Non-200 → SKIP + Seq s6b.customer.skipped

Abschluss-Event: s6b.run.completed mit Zählern (previewsSent, customersSent, rejected, regenerated, pending, errors)

🤖 S6b Bot 🖥️ Server
✅ Erfolgs-Flow: Kunde antwortet / kauft

Trigger: Kunde antwortet auf Mail oder klickt Angebot

  1. Eingehende Mail landet über {siteid}@support.minicon.eu im Support-Bot (support-bot.service.ts)
  2. Bot erkennt positive Antwort → setzt Label akquise-kontakt am Akquise-Ticket → S6b stoppt weitere Mails für dieses Ticket
  3. Bei konkretem Abschluss: Label akquise-manuell-verkauft / akquise-selbst
  4. Seq: s6b.cascade.ended mit endReason=kontakt
❌ Abbruch-Gründe

Greifen vor Phase A & vor Phase B:

  • akquise-kontakt — Kunde hat geantwortet
  • akquise-manuell-verkauft / akquise-selbst — Manuell geschlossen
  • akquise-pause — Michael hat abgebrochen (aus Reject-Form)
  • akquise-email-3 — Kaskade natürlich zu Ende
🌐 API-Endpoints (minicon-website-service)

Router src/routes/s6b.routes.ts — gemountet unter /api/s6b in src/app.ts. Cron-Endpoints mit X-Internal-Secret-Header; Admin-Endpoints token-gated (kein Login).

Cron-seitig (X-Internal-Secret)

POST/api/s6b/draft · Draft anlegen, Token + 4 URLs zurück
GET/api/s6b/draft/:token · Decision + HTML + Feedback abholen
POST/api/s6b/draft/:token/mark-sent · Nach Resend-Versand decision=sent setzen

Admin-seitig (Token in URL)

GET/api/s6b/admin-approve?token=…&action=approve · Direkte Freigabe
GET/api/s6b/admin-approve?token=…&action=reject · Reject-Formular
GET/api/s6b/admin-approve?token=…&action=edit · TinyMCE-Editor
GET/api/s6b/admin-approve?token=…&action=regenerate · Feedback-Formular
POST/api/s6b/admin-approve · Form-Submit (reject/edit/regenerate)
🗄️ MongoDB-Model: AkquiseEmailDraft

Datei: src/models/akquise-email-draft.model.ts — Source-of-Truth für den Approval-Lifecycle. Token-TTL 7 Tage, Index auf (ticketKey, stage, decision).

Felder: token (unique), siteId, ticketKey, stage (1|2|3), custEmail, custName, botName, subject, html, decision, decisionAt, rejectReason, regenerateFeedback, correlationId, expiresAt, sentAt, sentMessageId

decision-Lifecycle: pending → approved | rejected | regenerate → sent

Auto-Invalidierung: Beim Anlegen eines neuen Drafts für dasselbe (ticketKey, stage) werden vorherige pending-Drafts auf rejected (reason: superseded by newer draft) gesetzt.

📊 Seq-Event-Katalog (logging.minicon.eu)

CLEF-Format an ${SEQ_URL}/api/events/raw?clef. Helper: scripts/seq-log.ps1 (Write-SeqEvent, New-CorrelationId) + Server-seitig src/utils/logger.ts.

Standard-Properties auf jedem Event:

@t, @l, @mt, service (=s6b-email-timer), host, env, correlationId, siteId

Event Trigger Kern-Properties
s6b.run.startedCron-StartcorrelationId
s6b.preview.renderedTemplate gerendertstage, ticketKey, htmlLength
s6b.draft.createdMongoDB-Draft angelegtstage, draftId, htmlLength
s6b.preview.sentVorschau-Mail an Michaelstage, ticketKey, draftToken
s6b.preview.skippedPre-Send-Check failedreason
s6b.admin.decisionMichael klickt Buttondecision, approver, feedbackLength?, reasonLength?
s6b.admin.regenerateFeedback übernommenfeedbackLength
s6b.draft.editedTinyMCE-Edit gespeichertnewHtmlLength
s6b.dispatch.pendingDraft noch pending (Debug)ticketKey, stage
s6b.customer.sentMail an Kunde rausstage, custEmail, messageId
s6b.customer.skippedPreview-URL Non-200reason
s6b.customer.failedResend ≠ 200errorCode, errorMessage
s6b.cascade.endedPipeline-Ende pro TicketendReason (kontakt / email-3 / pause / verkauft)
s6b.run.completedCron-AbschlusspreviewsSent, customersSent, rejected, regenerated, pending, errors
📧 Email-Templates

Akquise-Kaskade:

📧 email-1-vorstellung.html · 📧 email-2-followup.html · 📧 email-3-letzte-chance.html

Platzhalter-Doku: PLATZHALTER.md

⚠️ {Anrede} ohne trailing Komma — Template hängt das Komma an. Sonst doppeltes ,,.

CR Workflow Emails: CR-Workflow PAP

🏷️ Labels & Zustände

Setup:

akquise-ticket · siteid-{slug} · region-dahn · akquise-bereit · akquise-vorbereitet

Kaskade (pro Stufe N=1|2|3):

akquise-email-{N}-preview-sent · akquise-email-{N}

Abbruch:

akquise-kontakt (Kunde antwortet) · akquise-pause (Reject) · akquise-manuell-verkauft · akquise-selbst

ℹ️ Alt-Labels akquise-email-preview-sent / akquise-email-approved werden aus Backward-Compat weiter akzeptiert, aber ab 2026-04-20 nicht mehr neu gesetzt.

🔗 Referenzen