Add daily activity digest — Phase B (v0.1.0:77)
Sends a once-a-day internal email to all active admins summarizing each team member's email activity per investor, plus a team-wide by-investor view (inbound + outbound, deduped). Narratives are generated on the LOCAL Spark model, never Claude — the digest is intentionally un-anonymized, so substance stays on Ten31 infra. This is an internal ops email, exempt from the 'agents draft, humans send' rule (which governs outward LP contact). - backend/digest_builder.py: per-user + per-investor activity queries (soft-delete filtered), per-user Spark narrative with a deterministic fallback, two-section plain-text body, and the DB-backed policy resolver. - backend/email_integration/digest_scheduler.py: always-on daily thread that re-reads the policy each cycle and sends once/day; window cursor in app_settings so a missed day rolls forward. - server.py: POST /api/admin/digest/send-now and GET/PATCH /api/admin/digest/policy; scheduler wired into main(). - Control lives in Settings -> Admin (enable toggle + send-time dropdown), not StartOS actions; env vars only seed the first-boot default. - Tests: backend/test_digest_builder.py.
This commit is contained in:
+39
-2
@@ -40,8 +40,45 @@ different category. **Never extend this path to send to LPs/prospects.**
|
||||
independent of any StartOS system-wide SMTP.
|
||||
- The admin **`POST /api/admin/digest/test-email`** restricts recipients to the active-admin
|
||||
set (not an open relay), and logs send failures rather than echoing them (an auth error can
|
||||
carry a token/credential). Digest *content* generation (Phase B) runs on **Spark, never
|
||||
Claude** — the digest is deliberately un-anonymized.
|
||||
carry a token/credential).
|
||||
|
||||
### Phase B — the daily digest itself (built)
|
||||
|
||||
- **Content builder: `backend/digest_builder.py`** (top-level). `build_digest(conn, since_iso,
|
||||
until_iso, chat_fn=None)` returns `{subject, body, has_activity, user_count, email_count,
|
||||
investor_count}` and composes **two sections**:
|
||||
- **By team member** — `collect_user_activity`: per registered user, both directions
|
||||
(per-mailbox `eam.is_sent`), with **one Spark narrative paragraph** per user
|
||||
(`ingest/llm.py` → Spark Control `/v1/chat/completions`), **never Claude** (the digest is
|
||||
deliberately un-anonymized — real LP names + substance stay local). Deterministic
|
||||
count-only fallback if Spark is unreachable (always-send must not fail).
|
||||
- **By investor** — `collect_investor_activity`: re-pivots the same window across the whole
|
||||
team, **deduped per email** (a reply to several teammates counts once), direction decided
|
||||
at the **email level** (outbound if `from_email` is one of our mailboxes, else inbound).
|
||||
Structured list, no extra Spark calls.
|
||||
- Soft-delete filters: `email_account_messages.deleted_at IS NULL` + `users.is_active = 1`,
|
||||
and the org/contact name joins drop soft-deleted rows (falling back to the matched address).
|
||||
- **Control is DB-backed, set from the admin panel** — `digest_builder.load_digest_policy(conn)`
|
||||
reads `app_settings.digest_policy` = `{enabled, send_hour}`. Precedence: **DB row wins**
|
||||
(the Settings → Admin toggle + send-time dropdown), else `CRM_DIGEST_ENABLED`/
|
||||
`CRM_DIGEST_SEND_HOUR` seed a first-boot default, else `{false, 18}`. `GET`/`PATCH
|
||||
/api/admin/digest/policy` (admin-only) read/write it. **Not a StartOS action** — it's an
|
||||
operational toggle, so it lives in-app where it's discoverable and takes effect live.
|
||||
- **Scheduler: `backend/email_integration/digest_scheduler.py`** (co-located with the sync
|
||||
scheduler). One daemon thread, **always started**; each cycle (60s) re-reads the DB policy
|
||||
and sends once per local day at/after `send_hour` **only when `enabled`** — so toggling in
|
||||
the panel takes effect with no restart. Content window = (last send, now]; cursor
|
||||
(`digest_last_sent_at`) + once-per-day guard (`digest_last_sent_date`) live in `app_settings`,
|
||||
so a missed day rolls into the next digest. Recipients = all active admins.
|
||||
- **On-demand: `POST /api/admin/digest/send-now`** (admin-only) → `maybe_send_digest(force=True)`
|
||||
builds the real last-24h digest and sends to the admin set regardless of the policy and
|
||||
**without** touching the daily cursor (a preview never suppresses the scheduled send).
|
||||
Surfaced as a "Send Digest Now" button in Settings → Admin, beside "Send Test Digest Email".
|
||||
- **Decisions (locked):** 6 PM default send · always-send (empty days get a "no activity"
|
||||
note) · per-user narrative + by-investor structured section · enable/time controlled in the
|
||||
admin panel. Tests: `backend/test_digest_builder.py` (per-user + per-investor queries,
|
||||
soft-delete, inbound dedup, two-section compose, fallback, policy resolver, scheduler guards
|
||||
— stubbed LLM + transport).
|
||||
|
||||
## Known gap
|
||||
|
||||
|
||||
Reference in New Issue
Block a user