Repurpose Communications tab as admin-only email-activity panel (v0.1.0:80)

The Communications tab is now an admin-only search over captured Gmail
(email_* tables), part of consolidating on the fundraising grid + email
capture as the canonical system of record.

- New GET /api/email/activity (admin-enforced server-side): filter by
  investor / mailbox / direction with free-text search over subject,
  snippet, and sender. Query logic in db.query_email_activity.
  - Soft-delete honored on the per-mailbox sighting (emails carry no
    deleted_at; deletion lives on email_account_messages).
  - Direction decided at the email level (outbound if the sender is one of
    our mailboxes), mirroring digest_builder.
  - Graveyard investors are hidden from the filter dropdown (CRM-wide
    graveyard=0 convention) but their email stays visible in the list and
    findable by free-text search — this is an audit surface.
- Communications page rewritten to render the panel; the classic manual
  "Log Communication" form is retired (the grid context menu remains the
  manual-log path). Nav item + page are admin-only.
- Tests: email_integration/test_email_activity_panel.py (filters,
  per-sighting soft-delete, roll-ups, graveyard handling, route 401/403);
  full suite 22/22. Frontend render verified via a jsdom mount smoke test
  plus the pinned classic-runtime Babel transform.

Code-only, no schema migration (version migrations are no-ops).
This commit is contained in:
Keysat
2026-06-16 14:49:59 -05:00
parent f9705d2216
commit 42d2b4b245
8 changed files with 494 additions and 291 deletions
+28
View File
@@ -33,6 +33,7 @@ from . import scheduler as _sched
_GET_ROUTES = {
"/api/email/status": "status",
"/api/email/accounts": "list_accounts",
"/api/email/activity": "activity",
"/api/email/threads": "list_threads",
"/api/email/oauth/start": "oauth_start",
"/api/email/oauth/callback": "oauth_callback",
@@ -187,6 +188,33 @@ def _h_list_accounts(handler):
handler.send_json({"accounts": rows})
def _h_activity(handler):
# Admin-only: the Communications page renders captured-Gmail activity (the classic
# manual-log surface was retired). Mailbox/investor substance is admin-scoped, so
# enforce admin server-side, not just nav-hide.
user = _require_admin(handler)
if not user:
return
q = handler.get_query_params()
try:
limit = int(q.get("limit", 100))
except (TypeError, ValueError):
limit = 100
conn = _conn()
try:
result = _db.query_email_activity(
conn,
investor_id=(q.get("investor_id") or "").strip() or None,
account_id=(q.get("account_id") or "").strip() or None,
search=(q.get("q") or q.get("search") or "").strip() or None,
direction=(q.get("direction") or "").strip() or None,
limit=limit,
)
finally:
conn.close()
handler.send_json(result)
def _h_list_threads(handler):
user = _require_auth(handler)
if not user: