Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Set up the Email AI-CRM module on a local or self-hosted Ring clone: configure channel secrets (or legacy IMAP_*), apply JSONB migrations when using Postgres, then verify ingest and draft send from the admin CRM UI (/admin/crm/*).
Prefer ring-config.emailCrm.channels + channel secrets. Copy the EMAIL CRM block from env.local.template:
# .env.local — multi-mailbox (recommended)
CRM_CHANNEL_PRIMARY_PASSWORD=
# CRM_CHANNEL_PRIMARY_SMTP_PASSWORD=
ANTHROPIC_API_KEY=
CRON_SECRET=generate-a-long-random-string
EMAIL_AUTO_SEND_ENABLED=false
WEBHOOK_EMAIL_SECRET= # optional: webhook ingest instead of IMAP
EMAIL_CRM_PERSISTENCE=memory # quick start without DB tables
# Auth OTP stays separate (lib/mailer.ts) — do not reuse for CRM replies:
# EMAIL_MODE=ethereal
# or SMTP_HOST / SMTP_USER / SMTP_PASSWORD for noreply@
# Legacy single-mailbox fallback (only when emailCrm.channels empty):
# IMAP_HOST=mail.example.com
# IMAP_PORT=993
# IMAP_TLS=true
# IMAP_USER=info@example.com
# IMAP_PASSWORD=
# SMTP_HOST=mail.example.com
# SMTP_PORT=587
# SMTP_USER=info@example.com
# SMTP_PASSWORD=Remove EMAIL_CRM_PERSISTENCE=memory and apply migrations below so threads and drafts persist across restarts.
Auth login mail uses SMTP_* / lib/mailer.ts. CRM draft send uses EmailSenderService + CRM_CHANNEL_*. See Ring Mailer.
When DATABASE_URL points at your dev Postgres:
psql "$DATABASE_URL" -f data/migrations/009_email_crm_jsonb.sql
psql "$DATABASE_URL" -f data/migrations/010_email_crm_tasks_jsonb.sql
psql "$DATABASE_URL" -c "\dt email_*"001_email_crm_schema.sql is deprecated. Always apply 009 + 010 only.
Option A — cron poll (matches production):
curl -sS -X POST "http://localhost:3000/api/cron/email-processor" \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'Option B — webhook (no IMAP required for the test):
BODY='{"messageId":"<test@local>","from":"you@example.com","to":"info@example.com","subject":"Hi","bodyText":"Hello"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_EMAIL_SECRET" | awk '{print $2}')
curl -X POST "http://localhost:3000/api/webhooks/email/inbound" \
-H "Content-Type: application/json" \
-H "X-Email-Webhook-Signature: $SIG" \
-d "$BODY"GET /api/admin/email/channels as admin → hasImapPassword / hasSmtpPassword).poll./admin/crm/inbox) — optional channel filter.email_messages.Only then set EMAIL_AUTO_SEND_ENABLED=true.
| Page | URL | Expect |
|---|---|---|
| Inbox | /{locale}/admin/crm/inbox | Live threads; channel filter; channel status chips |
| Thread | /{locale}/admin/crm/inbox/<threadId> | Message timeline |
| Drafts | /{locale}/admin/crm/drafts | Approve / reject / send |
| Contacts | /{locale}/admin/crm/contacts | CRM from processor |
| Analytics | /{locale}/admin/crm/analytics | Cost from email_api_usage |
| Tasks | /{locale}/admin/crm/tasks | Auto-created follow-ups |
All CRM pages use CrmAdminShell (horizontal tabs; no right rail). APIs remain /api/admin/email/*.
Schedule the same poll endpoint on your deployed BASE_URL every 1–5 minutes:
Optional analytics snapshot: GET $BASE_URL/api/cron/email-analytics with the same Bearer token.
Ops: data/migrations/EMAIL-CRM-OPS.md.
| Symptom | Check |
|---|---|
| Empty inbox | Migrations applied (or EMAIL_CRM_PERSISTENCE=memory)? Cron poll returning processed > 0? |
| Channel status red | CRM_CHANNEL_PRIMARY_PASSWORD set? ring-config.emailCrm.enabled true? |
| No AI draft | ANTHROPIC_API_KEY set? Security pipeline blocked? Check server logs. |
| Duplicate processing | Should not occur — email_messages Message-ID dedup |
| Draft send fails | Channel SMTP password / host (EmailSenderService), not Auth SMTP_* |
| Auto-send silent | EMAIL_AUTO_SEND_ENABLED=true and confidence rules met |
| 401 on cron | CRON_SECRET matches Authorization: Bearer header |
| Jsonb / circular require |
Set up the Email AI-CRM module on a local or self-hosted Ring clone: configure channel secrets (or legacy IMAP_*), apply JSONB migrations when using Postgres, then verify ingest and draft send from the admin CRM UI (/admin/crm/*).
Prefer ring-config.emailCrm.channels + channel secrets. Copy the EMAIL CRM block from env.local.template:
# .env.local — multi-mailbox (recommended)
CRM_CHANNEL_PRIMARY_PASSWORD=
# CRM_CHANNEL_PRIMARY_SMTP_PASSWORD=
ANTHROPIC_API_KEY=
CRON_SECRET=generate-a-long-random-string
EMAIL_AUTO_SEND_ENABLED=false
WEBHOOK_EMAIL_SECRET= # optional: webhook ingest instead of IMAP
EMAIL_CRM_PERSISTENCE=memory # quick start without DB tables
# Auth OTP stays separate (lib/mailer.ts) — do not reuse for CRM replies:
# EMAIL_MODE=ethereal
# or SMTP_HOST / SMTP_USER / SMTP_PASSWORD for noreply@
# Legacy single-mailbox fallback (only when emailCrm.channels empty):
# IMAP_HOST=mail.example.com
# IMAP_PORT=993
# IMAP_TLS=true
# IMAP_USER=info@example.com
# IMAP_PASSWORD=
# SMTP_HOST=mail.example.com
# SMTP_PORT=587
# SMTP_USER=info@example.com
# SMTP_PASSWORD=Remove EMAIL_CRM_PERSISTENCE=memory and apply migrations below so threads and drafts persist across restarts.
Auth login mail uses SMTP_* / lib/mailer.ts. CRM draft send uses EmailSenderService + CRM_CHANNEL_*. See Ring Mailer.
When DATABASE_URL points at your dev Postgres:
psql "$DATABASE_URL" -f data/migrations/009_email_crm_jsonb.sql
psql "$DATABASE_URL" -f data/migrations/010_email_crm_tasks_jsonb.sql
psql "$DATABASE_URL" -c "\dt email_*"001_email_crm_schema.sql is deprecated. Always apply 009 + 010 only.
Option A — cron poll (matches production):
curl -sS -X POST "http://localhost:3000/api/cron/email-processor" \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'Option B — webhook (no IMAP required for the test):
BODY='{"messageId":"<test@local>","from":"you@example.com","to":"info@example.com","subject":"Hi","bodyText":"Hello"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_EMAIL_SECRET" | awk '{print $2}')
curl -X POST "http://localhost:3000/api/webhooks/email/inbound" \
-H "Content-Type: application/json" \
-H "X-Email-Webhook-Signature: $SIG" \
-d "$BODY"GET /api/admin/email/channels as admin → hasImapPassword / hasSmtpPassword).poll./admin/crm/inbox) — optional channel filter.email_messages.Only then set EMAIL_AUTO_SEND_ENABLED=true.
| Page | URL | Expect |
|---|---|---|
| Inbox | /{locale}/admin/crm/inbox | Live threads; channel filter; channel status chips |
| Thread | /{locale}/admin/crm/inbox/<threadId> | Message timeline |
| Drafts | /{locale}/admin/crm/drafts | Approve / reject / send |
| Contacts | /{locale}/admin/crm/contacts | CRM from processor |
| Analytics | /{locale}/admin/crm/analytics | Cost from email_api_usage |
| Tasks | /{locale}/admin/crm/tasks | Auto-created follow-ups |
All CRM pages use CrmAdminShell (horizontal tabs; no right rail). APIs remain /api/admin/email/*.
Schedule the same poll endpoint on your deployed BASE_URL every 1–5 minutes:
Optional analytics snapshot: GET $BASE_URL/api/cron/email-analytics with the same Bearer token.
Ops: data/migrations/EMAIL-CRM-OPS.md.
| Symptom | Check |
|---|---|
| Empty inbox | Migrations applied (or EMAIL_CRM_PERSISTENCE=memory)? Cron poll returning processed > 0? |
| Channel status red | CRM_CHANNEL_PRIMARY_PASSWORD set? ring-config.emailCrm.enabled true? |
| No AI draft | ANTHROPIC_API_KEY set? Security pipeline blocked? Check server logs. |
| Duplicate processing | Should not occur — email_messages Message-ID dedup |
| Draft send fails | Channel SMTP password / host (EmailSenderService), not Auth SMTP_* |
| Auto-send silent | EMAIL_AUTO_SEND_ENABLED=true and confidence rules met |
| 401 on cron | CRON_SECRET matches Authorization: Bearer header |
| Jsonb / circular require |
Set up the Email AI-CRM module on a local or self-hosted Ring clone: configure channel secrets (or legacy IMAP_*), apply JSONB migrations when using Postgres, then verify ingest and draft send from the admin CRM UI (/admin/crm/*).
Prefer ring-config.emailCrm.channels + channel secrets. Copy the EMAIL CRM block from env.local.template:
# .env.local — multi-mailbox (recommended)
CRM_CHANNEL_PRIMARY_PASSWORD=
# CRM_CHANNEL_PRIMARY_SMTP_PASSWORD=
ANTHROPIC_API_KEY=
CRON_SECRET=generate-a-long-random-string
EMAIL_AUTO_SEND_ENABLED=false
WEBHOOK_EMAIL_SECRET= # optional: webhook ingest instead of IMAP
EMAIL_CRM_PERSISTENCE=memory # quick start without DB tables
# Auth OTP stays separate (lib/mailer.ts) — do not reuse for CRM replies:
# EMAIL_MODE=ethereal
# or SMTP_HOST / SMTP_USER / SMTP_PASSWORD for noreply@
# Legacy single-mailbox fallback (only when emailCrm.channels empty):
# IMAP_HOST=mail.example.com
# IMAP_PORT=993
# IMAP_TLS=true
# IMAP_USER=info@example.com
# IMAP_PASSWORD=
# SMTP_HOST=mail.example.com
# SMTP_PORT=587
# SMTP_USER=info@example.com
# SMTP_PASSWORD=Remove EMAIL_CRM_PERSISTENCE=memory and apply migrations below so threads and drafts persist across restarts.
Auth login mail uses SMTP_* / lib/mailer.ts. CRM draft send uses EmailSenderService + CRM_CHANNEL_*. See Ring Mailer.
When DATABASE_URL points at your dev Postgres:
psql "$DATABASE_URL" -f data/migrations/009_email_crm_jsonb.sql
psql "$DATABASE_URL" -f data/migrations/010_email_crm_tasks_jsonb.sql
psql "$DATABASE_URL" -c "\dt email_*"001_email_crm_schema.sql is deprecated. Always apply 009 + 010 only.
Option A — cron poll (matches production):
curl -sS -X POST "http://localhost:3000/api/cron/email-processor" \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'Option B — webhook (no IMAP required for the test):
BODY='{"messageId":"<test@local>","from":"you@example.com","to":"info@example.com","subject":"Hi","bodyText":"Hello"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_EMAIL_SECRET" | awk '{print $2}')
curl -X POST "http://localhost:3000/api/webhooks/email/inbound" \
-H "Content-Type: application/json" \
-H "X-Email-Webhook-Signature: $SIG" \
-d "$BODY"GET /api/admin/email/channels as admin → hasImapPassword / hasSmtpPassword).poll./admin/crm/inbox) — optional channel filter.email_messages.Only then set EMAIL_AUTO_SEND_ENABLED=true.
| Page | URL | Expect |
|---|---|---|
| Inbox | /{locale}/admin/crm/inbox | Live threads; channel filter; channel status chips |
| Thread | /{locale}/admin/crm/inbox/<threadId> | Message timeline |
| Drafts | /{locale}/admin/crm/drafts | Approve / reject / send |
| Contacts | /{locale}/admin/crm/contacts | CRM from processor |
| Analytics | /{locale}/admin/crm/analytics | Cost from email_api_usage |
| Tasks | /{locale}/admin/crm/tasks | Auto-created follow-ups |
All CRM pages use CrmAdminShell (horizontal tabs; no right rail). APIs remain /api/admin/email/*.
Schedule the same poll endpoint on your deployed BASE_URL every 1–5 minutes:
Optional analytics snapshot: GET $BASE_URL/api/cron/email-analytics with the same Bearer token.
Ops: data/migrations/EMAIL-CRM-OPS.md.
| Symptom | Check |
|---|---|
| Empty inbox | Migrations applied (or EMAIL_CRM_PERSISTENCE=memory)? Cron poll returning processed > 0? |
| Channel status red | CRM_CHANNEL_PRIMARY_PASSWORD set? ring-config.emailCrm.enabled true? |
| No AI draft | ANTHROPIC_API_KEY set? Security pipeline blocked? Check server logs. |
| Duplicate processing | Should not occur — email_messages Message-ID dedup |
| Draft send fails | Channel SMTP password / host (EmailSenderService), not Auth SMTP_* |
| Auto-send silent | EMAIL_AUTO_SEND_ENABLED=true and confidence rules met |
| 401 on cron | CRON_SECRET matches Authorization: Bearer header |
| Jsonb / circular require |
| Orders | /{locale}/admin/crm/orders | Project orders desk (Owner Project Lab) |
Types in features/email-crm/types/*; no CJS require() of Jsonb repos |
Same-workflow: kingdom migration order and run-migration.sh.
curl -sS -X POST "$BASE_URL/api/cron/email-processor" \
-H "Authorization: Bearer ${CRON_SECRET}" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'| Orders | /{locale}/admin/crm/orders | Project orders desk (Owner Project Lab) |
Types in features/email-crm/types/*; no CJS require() of Jsonb repos |
Same-workflow: kingdom migration order and run-migration.sh.
curl -sS -X POST "$BASE_URL/api/cron/email-processor" \
-H "Authorization: Bearer ${CRON_SECRET}" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'| Orders | /{locale}/admin/crm/orders | Project orders desk (Owner Project Lab) |
Types in features/email-crm/types/*; no CJS require() of Jsonb repos |
Same-workflow: kingdom migration order and run-migration.sh.
curl -sS -X POST "$BASE_URL/api/cron/email-processor" \
-H "Authorization: Bearer ${CRON_SECRET}" \
-H "Content-Type: application/json" \
-d '{"action":"poll"}'