3-stufige Email-Kampagne mit Web-Approval, MongoDB-Draft & Seq-Logging — Stand 2026-04-20
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.
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.
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
project=DAHN AND labels="build-ticket" AND labels="po-approved"node C:\working\atlas\scripts\screenshot.js https://{slug}.minicon.eu → {slug}.jpg[Akquise] {Firmenname}akquise-ticket, siteid-{slug}, akquise-bereitwebsite-webseiten/src/app/page.tsx eintragen (2 Keys) + commit + push → GitHub-Action deployed auf Cloudflare Pagesakquise-bereit → akquise-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
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).
JQL (Stufe 1):
JQL (Stufe 2): … labels="akquise-email-1" AND updated < -7d
JQL (Stufe 3): … labels="akquise-email-2" AND updated < -7d
Aktionen:
pre-send-check.ps1 -SiteId {slug} — Non-200 → SKIP + Seq-Event s6b.preview.skippedGET api.minicon.eu/api/admin/customers/{siteId} → customer.email, botPersona.name, company, contacts[0].nameemail-{N}-{typ}.html mit Platzhaltern {Anrede} (ohne trailing Komma!), {firmenname}, {preview_url}, {angebots_url}, {bot_name}, {features}/de/ — falls 200, Pfad erweitern (für mehrsprachige Sites wie Felsland)[S6b-Feedback]-Kommentar seit letzter Preview → Tonfall/Features anpassen, Kommentar quittierenX-Internal-Secret: $SECRET_HUB_INTERNAL_SECRET{siteId, ticketKey, stage, custEmail, custName, botName, subject, html, correlationId}{token, approve, reject, edit, regenerate, expiresAt}
noreply@support.minicon.eu, To: michael.nikolaus@minicon.eu):
api.minicon.eu/api/s6b/admin-approve?token=…&action=…
email-{N}-{siteid}.htmlakquise-email-{N}-preview-sents6b.preview.rendered, s6b.preview.sent (correlationId, siteId, ticketKey, stage)Trigger: Michael erhält Vorschau-Mail, klickt Button
Token-TTL: 7 Tage
Aktionen:
decision=approved. Bestätigungsseite. Nächster Cron-Run versendet.decision=rejected, rejectReason gespeichert.html → Draft updated. Anschließend Approve/Reject/Regenerate möglich.
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 🖥️ ServerJQL: labels="akquise-email-*-preview-sent" AND labels NOT IN (akquise-kontakt, akquise-manuell-verkauft, akquise-selbst, akquise-pause)
Pro Ticket:
[S6b]-Kommentar extrahierendecision:
draft.custEmail (CC Michael) · POST /api/s6b/draft/{token}/mark-sent · Label akquise-email-{N} + Preview-Label entfernen · Seq s6b.customer.sentakquise-pause · Jira-Kommentar mit rejectReason · Seq s6b.cascade.ended mit endReason=rejected[S6b-Feedback] … · Preview-Label entfernen (Ticket zurück in Phase A) · Seq s6b.admin.regenerates6b.dispatch.pendings6b.customer.failed · KEIN Label-Update (Cron retry beim nächsten Run)curl -I {preview_url} — Non-200 → SKIP + Seq s6b.customer.skippedAbschluss-Event: s6b.run.completed mit Zählern (previewsSent, customersSent, rejected, regenerated, pending, errors)
Trigger: Kunde antwortet auf Mail oder klickt Angebot
{siteid}@support.minicon.eu im Support-Bot (support-bot.service.ts)akquise-kontakt am Akquise-Ticket → S6b stoppt weitere Mails für dieses Ticketakquise-manuell-verkauft / akquise-selbsts6b.cascade.ended mit endReason=kontaktGreifen vor Phase A & vor Phase B:
akquise-kontakt — Kunde hat geantwortetakquise-manuell-verkauft / akquise-selbst — Manuell geschlossenakquise-pause — Michael hat abgebrochen (aus Reject-Form)akquise-email-3 — Kaskade natürlich zu EndeRouter 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).
decision=sent setzenDatei: 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.
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.started | Cron-Start | correlationId |
s6b.preview.rendered | Template gerendert | stage, ticketKey, htmlLength |
s6b.draft.created | MongoDB-Draft angelegt | stage, draftId, htmlLength |
s6b.preview.sent | Vorschau-Mail an Michael | stage, ticketKey, draftToken |
s6b.preview.skipped | Pre-Send-Check failed | reason |
s6b.admin.decision | Michael klickt Button | decision, approver, feedbackLength?, reasonLength? |
s6b.admin.regenerate | Feedback übernommen | feedbackLength |
s6b.draft.edited | TinyMCE-Edit gespeichert | newHtmlLength |
s6b.dispatch.pending | Draft noch pending (Debug) | ticketKey, stage |
s6b.customer.sent | Mail an Kunde raus | stage, custEmail, messageId |
s6b.customer.skipped | Preview-URL Non-200 | reason |
s6b.customer.failed | Resend ≠ 200 | errorCode, errorMessage |
s6b.cascade.ended | Pipeline-Ende pro Ticket | endReason (kontakt / email-3 / pause / verkauft) |
s6b.run.completed | Cron-Abschluss | previewsSent, customersSent, rejected, regenerated, pending, errors |
Akquise-Kaskade:
📧 email-1-vorstellung.html · 📧 email-2-followup.html · 📧 email-3-letzte-chance.htmlPlatzhalter-Doku: PLATZHALTER.md
⚠️ {Anrede} ohne trailing Komma — Template hängt das Komma an. Sonst doppeltes ,,.
CR Workflow Emails: CR-Workflow PAP
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.
src/routes/s6b.routes.ts, src/models/akquise-email-draft.model.ts)