Add Django site, Docker packaging, and beta/prod Gitea deploys.
Deploy Beta / unit-tests (push) Successful in 9s
Deploy Beta / docker (push) Successful in 17s
Deploy Beta / deploy-beta (push) Successful in 2m31s

Unignore site/ (was blocked by mkdocs /site rule), add compose/Docker/uv tooling, and split deploys so push to main goes to beta while prod stays manual.
This commit is contained in:
2026-08-08 07:32:55 -05:00
parent 7dca98bbf6
commit 1f7d78de64
204 changed files with 21662 additions and 70 deletions
+35 -29
View File
@@ -23,7 +23,7 @@ multiple active/active instances behind Nginx Proxy Manager (NPM).
composer, social scheduler.
- One mailing list of contacts. Form submissions auto-create/append contacts.
- Per-channel, per-contact consent with easy opt-out (email unsubscribe link, SMS `STOP`).
- Outreach over Email + SMS (SMTP2GO) and Postcard (pluggable provider, default Lob).
- Outreach over Email + SMS (SMTP2GO) and Postcard (PCM Integrations, pluggable).
- Social posting/scheduling to Facebook, Instagram, LinkedIn via native APIs.
- Cheaper than PostcardMania + adds social automation.
@@ -74,7 +74,7 @@ flowchart TB
subgraph external [External APIs]
SMTP2GO["SMTP2GO (email + SMS)"]
Postcard["Postcard provider (Lob default)"]
Postcard["Postcard provider (PCM Integrations)"]
Meta["Meta Graph API (FB + IG)"]
LinkedIn["LinkedIn API"]
Recaptcha["Google reCAPTCHA"]
@@ -230,30 +230,34 @@ a contact opt out of SMS while keeping email.
writes a `Suppression`. A2P 10DLC registration is a client onboarding prerequisite (call out
in Phase 3).
### 6.2 Postcards — pluggable provider (default Lob)
### 6.2 Postcards — PCM Integrations (pluggable)
A small provider interface so the realtor isn't locked in and we can shop on price:
Default provider is **PCM Integrations** (DirectMail API v3). A small provider interface
keeps Lob / Click2Mail / PostGrid available behind `POSTCARD_PROVIDER` if needed later.
```python
class PostcardProvider(Protocol):
def send_postcard(self, *, to: PostalAddress, from_: PostalAddress,
front: Asset, back: Asset, idempotency_key: str) -> ProviderResult: ...
def get_status(self, provider_id: str) -> DeliveryStatus: ...
def send_postcard(self, message) -> PostcardResult: ...
def get_status(self, provider_id: str) -> str: ...
```
Adapters live in `messaging/providers/postcard/`. Selected via `POSTCARD_PROVIDER` env var.
| Provider | Model | ~4x6 postcard | Best for | Notes |
|----------|-------|---------------|----------|-------|
| **Lob** (default) | API-first, tiered | Free dev tier ~$0.77; ~$0.51 on $260/mo Startup | Clean API, address verification, in-transit tracking | Best DX; monthly fee only worth it at volume |
| Click2Mail | Pay-per-piece, no subscription | ~$0.350.70 | Low/occasional volume, no monthly fee | API less polished; great when volume is small |
| PostGrid | API + dashboard, subscription | Contact sales (from ~$250/mo) | Compliance-heavy, templates | Overkill unless compliance-driven |
| Stannp | Marketer-friendly, no minimums | Transparent per-piece | Non-dev fallback, EU | Good dashboard |
| USPS EDDM | Postage-only saturation | Postage only | Whole-route blasts (no list) | No per-address API; manual/bulk option |
| Provider | Model | Notes |
|----------|-------|-------|
| **PCM Integrations** (default) | DirectMail API v3 + embedded designer | Iframe editor (`POST /design/custom`, embed URL); orders with `designID` + recipients; inbound status webhooks |
| Lob | API-first, tiered | Alternate behind same interface |
| Click2Mail | Pay-per-piece | Low-volume alternate |
| PostGrid | API + dashboard | Compliance-heavy alternate |
> Recommendation: start on **Lob Developer (free) tier** to validate, keep **Click2Mail** as the
> low-volume cost option behind the same interface. Revisit once monthly volume is known — the
> abstraction makes switching a config change.
**Designer:** portal embeds PCMs editor (no homemade layout tool). Saved designs become
`MessageTemplate` rows with `postcard_front.design_id`.
**Webhooks:** `POST /portal/messaging/webhooks/postcard/` with Bearer `PCM_WEBHOOK_SECRET`.
Correlate via `extRefNbr` (= `Message.pk`) or `orderID` (= `provider_message_id`).
**Campaign notify:** when any channel campaign reaches `completed`, one summary email goes
to `created_by.email` or `CONTACT_EMAIL` (`Campaign.notify_sent_at` guard).
### 6.3 Social — native APIs (Facebook, Instagram, LinkedIn)
Chosen over an aggregator (Ayrshare) to avoid per-profile monthly fees, since this is one
@@ -324,10 +328,10 @@ monica_site/ # repo root
│ ├── worker-entrypoint.sh # django.tasks backend worker (+ optional dispatch_due loop)
│ └── validate-env.sh
├── .gitea/workflows/
│ ├── ci.yml # PR: uv sync + manage.py test
│ ├── ci.yml # PR: uv sync + cd site && manage.py test
│ ├── unittests.yml # master: tests gate deploy
│ └── deploy.yml # calls server-infra/scripts/deploy.sh --app monica_site
└── monica_site/ # Django project dir (manage.py lives here)
└── site/ # Django project dir (manage.py lives here)
├── manage.py
├── monica_site/ # project package
│ ├── __init__.py
@@ -358,7 +362,7 @@ monica_site/ # repo root
│ └── providers/
│ ├── email/smtp2go.py
│ ├── sms/smtp2go.py
│ └── postcard/{base.py,lob.py,click2mail.py,postgrid.py}
│ └── postcard/{__init__.py,pcm.py,lob.py,click2mail.py,postgrid.py}
└── social/ # accounts, posts, scheduling
├── models.py
├── tasks.py # @task publish_social_target, etc.
@@ -409,7 +413,7 @@ monica_site:
### 8.3 CI/CD flow
Same as `company_site`:
1. PR → `ci.yml` runs `uv sync` + `manage.py test`.
1. PR → `ci.yml` runs `uv sync` + `cd site && manage.py test`.
2. Merge to `main` → `unittests.yml` (containerized tests) → on green, `deploy.yml` calls
`server-infra/scripts/deploy.sh --app monica_site --env prod --ref <sha>`.
3. `deploy-apps.yml` checks out the ref on each app host, injects `.env`, `docker compose build`,
@@ -464,9 +468,11 @@ EMAIL_USE_TLS=true
# SMTP2GO SMS
SMTP2GO_SMS_API_KEY=...
# Postcards
POSTCARD_PROVIDER=lob # lob|click2mail|postgrid
LOB_API_KEY=...
# Postcards (PCM Integrations)
POSTCARD_PROVIDER=pcm # pcm|lob|click2mail|postgrid
PCM_API_KEY=...
PCM_WEBHOOK_SECRET=...
PCM_RETURN_ADDRESS={...}
# Social (native)
META_APP_ID=...
@@ -512,11 +518,11 @@ TIANJI_WEBSITE_ID=...
- **Exit:** realtor sends a real email + SMS campaign to consented contacts; opt-out works.
### Phase 3 — Postcards
- `messaging/providers/postcard/` interface + Lob adapter (+ Click2Mail adapter as the
low-volume option); `POSTCARD_PROVIDER` switch.
- Postcard campaign flow: pick template/artwork, select recipients (address required + Lob
address verification), enqueue batch as Django Tasks, poll delivery status.
- Client prerequisites called out: Lob account, A2P 10DLC for SMS.
- `messaging/providers/postcard/` + PCM Integrations adapter (Lob/Click2Mail kept as
alternates); `POSTCARD_PROVIDER=pcm`.
- PCM iframe designer; save `design_id` on `MessageTemplate`; postcard campaign audience;
inbound PCM webhooks → `ProviderEvent`; campaign completion notify email.
- Client prerequisites: PCM Integrations account + API key, A2P 10DLC for SMS.
- **Exit:** realtor mails a postcard batch and sees delivery tracking.
### Phase 4 — Social automation