Backend-Lag-Fix Phase 1 — entlastet die CPU-Dauerschleife im Mail-Stack:
- delete mail-scan-cron.ts: der 30-Min-Nitro-Cron scannte alle User parallel
(Promise.allSettled) und war redundant zum IMAP-IDLE-Daemon (Single Source
of Truth). Reine Dauerlast ohne Mehrwert.
- imap-idle: In-Flight-Guard (scanInFlight + coalescePending). triggerScan ist
jetzt re-entry-safe — pro Connection max. 1 aktiver + 1 pending Scan statt
bis zu 8 gestapelt pro 2-Min-NOOP-Tick. Gilt für NOOP + exists-Event.
- plan-features: Pro mailAgents 3->2 (+ Math.min-Hack in coach/message aufgeräumt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TTS-Sprache war provider-übergreifend hart auf "de" verdrahtet, locale aus dem
Request wurde ignoriert → arabischer Text wurde deutsch-phonetisch gesprochen.
- locale aus Body auslesen → Basis-Sprachcode an alle Provider
- Pro: Cartesia sonic-2 → sonic-3 (sonic-2 kann kein Arabisch; sonic-3 = 42 Sprachen)
- Legend: ElevenLabs language_code gesetzt (turbo_v2_5 multilingual, ar dabei)
- Google-Fallback: BCP-47-Map (ar→ar-XA etc.), de-Voice nur noch für de
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Custom-Domain-Slots sind jetzt EIN gemeinsamer Pool für web + mail
(Pro 10 / Legend 20) statt getrennter web/mail-Buckets. Free-Tier ist
entfallen — PLAN_LIMITS hat nur noch pro + legend, getPlanLimits
defaultet auf pro.
Backend:
- plan-features: customDomains ist eine Zahl (CustomDomainLimits weg)
- index.post: Slot-Check gegen Gesamt-Count, Fehler einheitlich LIMIT_REACHED
- index.get: liefert { items, count, limit }
- change-preview + coach/message an die neue Form angepasst
Frontend:
- useCustomDomains: count/limit (Zahlen) statt countsByType/limits
- AddDomainSheet: ein generischer Limit-Hinweis (error_limit_reached)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
plan-features.customDomains is now { web, mail } per plan instead of a
single number. Free 5+5, Pro 5+5, Legend 10+10 — the user explicitly
chose separate pools so users don't have to trade a website slot for a
mail-pattern slot or vice versa.
- countActiveCustomDomainsSplit(userId) groupBy type → { web, mail }
(mail aggregates mail_domain + mail_display_name). Old single-count
function stays as a deprecated alias for any caller still on it.
- POST /api/custom-domains: body-compat accepts both { pattern, kind }
(current frontend) and { domain, type } (legacy / direct). kind='mail'
is split into mail_domain vs mail_display_name server-side based on
whether the pattern looks like a domain. Slot check is per-bucket;
errors are WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED so the UI can show
the right limit-reached message per tab.
- GET /api/custom-domains: response shape extended to
{ items, counts: { web, mail }, limits: { web, mail } } so the
frontend can drive the per-tab counter without client-side estimation.
- POST /api/custom-domains/:id/submit: hard-blocks mail_display_name
with 400 DISPLAY_NAME_NOT_SUBMITTABLE. Display-name submission to the
global blocklist is deferred to v1.1 — would require a schema split
on BlocklistDomain that's risky pre-TestFlight. mail_domain still
flows through the community-vote pipeline like web entries.
- auth/me.get.ts, plan/change-preview.get.ts, coach/message.post.ts
updated for the new shape (Lyra prompts untouched, only template
variables split web vs mail counts).
24 vitest cases in backend/tests/custom-domains/plan-limits.test.ts
cover the new shape, body compat, bucket logic, and the submit guard;
216/216 total backend tests pass.