Email search/query + windowed digest preview (v0.1.0:83)
Communications tab (search/query roadmap items 1 & 2): - Fix the investor dropdown: the facet only listed grid investors, so it came back empty whenever email matched a classic contact or org domain (no grid id — the common case). It now mirrors the email list, resolving each link to a typed identity (fund:/org:/contact:/addr:) with precedence grid -> org -> contact -> address; investor_id accepts the typed key (bare id = fund: for back-compat) and an unknown prefix matches nothing. - Add a date-range filter and a click-to-expand full-body view (GET /api/email/detail, admin, soft-delete-gated; body_text only, never raw remote HTML). - Add a "Search content" mode: GET /api/email/search wraps the ingest hybrid_search over the Qdrant email index (doc_type=email), hydrated and soft-delete-filtered against SQLite (canonical), 503 if Spark/Qdrant down. Daily digest: - Settings -> Admin builds a digest over a chosen window (last 24h or since a date) as an in-app preview before sending (POST /api/admin/digest/preview), so the local-Spark summarizer can be verified on demand even on a quiet day. Manual send uses the same window; neither advances the daily cursor, so a preview never suppresses the scheduled digest. Code-only, migrations no-op. 22/22 backend tests, render-smoke pass.
This commit is contained in:
+43
-9
@@ -1914,6 +1914,8 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
return self.handle_admin_send_test_email(user, body)
|
||||
if path == '/api/admin/digest/send-now':
|
||||
return self.handle_admin_send_digest_now(user, body)
|
||||
if path == '/api/admin/digest/preview':
|
||||
return self.handle_admin_digest_preview(user, body)
|
||||
if path == '/api/fundraising/backup':
|
||||
return self.handle_backup_fundraising_state(user)
|
||||
if path == '/api/fundraising/restore-preview':
|
||||
@@ -4140,18 +4142,50 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
|
||||
return self.send_json({"data": {"status": "sent", **result}})
|
||||
|
||||
def handle_admin_send_digest_now(self, user, body):
|
||||
"""Build the REAL daily activity digest (last 24h) on demand and send it to
|
||||
the active-admin set now. An on-demand preview of Phase B — it does not
|
||||
touch the daily schedule's cursor, so it never suppresses the scheduled send.
|
||||
Content is summarized on Spark (local), never Claude."""
|
||||
def handle_admin_digest_preview(self, user, body):
|
||||
"""Build the activity digest over a chosen window and return it WITHOUT
|
||||
sending — the admin-panel preview. Window defaults to the last 24h, or
|
||||
{hours: N} / {since: 'YYYY-MM-DD'} (a local date -> that day's midnight).
|
||||
Runs the REAL Spark summarization, so widening the window is how you verify
|
||||
the summarizer on a quiet day. Never touches the daily cursor."""
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin only", 403)
|
||||
|
||||
import digest_mailer
|
||||
body = body or {}
|
||||
import digest_builder
|
||||
try:
|
||||
from email_integration.digest_scheduler import maybe_send_digest
|
||||
result = maybe_send_digest(force=True)
|
||||
since_iso, until_iso = digest_builder.resolve_digest_window(
|
||||
hours=body.get('hours'), since=body.get('since'))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(str(exc), 400)
|
||||
conn = get_db()
|
||||
try:
|
||||
digest = digest_builder.build_digest(conn, since_iso, until_iso)
|
||||
except Exception as exc:
|
||||
print(f"[digest] preview failed: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return self.send_error_json("Preview failed — see server logs for details.", 502)
|
||||
finally:
|
||||
conn.close()
|
||||
return self.send_json({"data": {**digest, "window": [since_iso, until_iso]}})
|
||||
|
||||
def handle_admin_send_digest_now(self, user, body):
|
||||
"""Build the REAL activity digest over a chosen window and send it to the
|
||||
active-admin set now. Window defaults to the last 24h, or {hours: N} /
|
||||
{since: 'YYYY-MM-DD'} — same resolution as the preview. Does NOT touch the
|
||||
daily schedule's cursor, so it never suppresses the scheduled send. Content
|
||||
is summarized on Spark (local), never Claude."""
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin only", 403)
|
||||
body = body or {}
|
||||
import digest_mailer
|
||||
import digest_builder
|
||||
try:
|
||||
since_iso, until_iso = digest_builder.resolve_digest_window(
|
||||
hours=body.get('hours'), since=body.get('since'))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(str(exc), 400)
|
||||
try:
|
||||
from email_integration.digest_scheduler import send_digest_window
|
||||
result = send_digest_window(since_iso=since_iso, until_iso=until_iso)
|
||||
except digest_mailer.NoTransport as exc:
|
||||
return self.send_error_json(str(exc), 400)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user