Refine email-proposal review UX (v0.1.0:91)

Three post-smoke refinements to the Matrix email-proposal review:

1. Dash separators (bot): every card/reply is framed with a dash rule top and
   bottom so threads stop bleeding together vertically on mobile.

2. Remove decided threads (bot): on a conclusive approve/dismiss from either
   surface, the bot redacts the card (client.room_redact) so the room clears
   down to only undecided items. Redacting the bot's own card needs no power;
   the web->Matrix path now redacts instead of posting a closure note.

3. Clearer note wording (server v91 + bot): the proposed grid note now names who
   emailed whom -- "{teammate} emailed {investor}" (outbound) / "{sender} emailed
   the team" (inbound) -- instead of an ambiguous "Sent"/"Received". Outbound
   detection also matches our corporate domain (public providers excluded), so a
   teammate's mail from a non-enrolled @ten31.xyz address no longer reads as
   "Received". Going-forward only; no schema change. The card drops its bare
   direction label since the note now carries the relationship.

Tests updated; 30/30 green, render-smoke green.
This commit is contained in:
Keysat
2026-06-18 11:59:38 -05:00
parent 48bd29aaa3
commit a10889b10b
9 changed files with 136 additions and 51 deletions
+47 -5
View File
@@ -5731,8 +5731,45 @@ def _append_grid_note(conn, inv_id, inv_name, note, updated_by=None):
return True
def _activity_note_text(sent_at, direction, gist):
return f"{_ACTIVITY_MARKER} {_fmt_activity_date(sent_at)}{direction}: {gist}"
# Public mailbox providers: a teammate's enrolled gmail must NOT make every gmail sender read
# as "ours" (that would flip inbound LP gmail to outbound). Only corporate domains imply us.
_PUBLIC_EMAIL_DOMAINS = {
"gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com", "msn.com",
"yahoo.com", "ymail.com", "icloud.com", "me.com", "mac.com", "aol.com",
"proton.me", "protonmail.com", "pm.me",
}
def _our_email_domains(own_addresses):
"""Corporate domains we send from, derived from the enrolled mailboxes (public providers
excluded). Lets an @ten31.xyz sender read as outbound even when that mailbox isn't enrolled —
the misclassification behind a teammate's email showing as 'Received'."""
out = set()
for a in own_addresses:
if "@" in a:
dom = a.split("@", 1)[1]
if dom and dom not in _PUBLIC_EMAIL_DOMAINS:
out.add(dom)
return out
def _sender_display(from_name, from_email):
"""A human name for the sender: their display name, else the email's local part, else a stub."""
if from_name and from_name.strip():
return from_name.strip()
if from_email and "@" in from_email:
return from_email.split("@", 1)[0]
return (from_email or "").strip() or "Someone"
def _activity_note_text(sent_at, outbound, gist, sender, investor_name):
"""Name WHO emailed WHOM rather than a bare Sent/Received (which left 'who received?'
ambiguous): outbound -> "{teammate} emailed {investor}", inbound -> "{sender} emailed the team"."""
if outbound:
rel = f"{sender} emailed {investor_name}" if investor_name else f"{sender} sent an email"
else:
rel = f"{sender} emailed the team"
return f"{_ACTIVITY_MARKER} {_fmt_activity_date(sent_at)}{rel}: {gist}"
def propose_email_activity_notes(limit=50):
@@ -5752,7 +5789,7 @@ def propose_email_activity_notes(limit=50):
conn.commit()
try:
rows = conn.execute(
"SELECT id, subject, body_text, snippet, from_email, sent_at FROM emails "
"SELECT id, subject, body_text, snippet, from_name, from_email, sent_at FROM emails "
"WHERE is_matched=1 AND sent_at >= ? "
"AND id NOT IN (SELECT email_id FROM email_activity_proposals) "
"ORDER BY sent_at ASC LIMIT ?", (since, limit)).fetchall()
@@ -5765,14 +5802,19 @@ def propose_email_activity_notes(limit=50):
own = {(r[0] or "").lower() for r in conn.execute("SELECT email_address FROM email_accounts")}
except Exception:
pass
our_domains = _our_email_domains(own)
done = 0
for r in rows:
inv_id, inv_name = _activity_investor(conn, r["id"])
direction = "Sent" if (r["from_email"] or "").lower() in own else "Received"
from_email = (r["from_email"] or "").lower()
# Outbound = sent by us: an enrolled mailbox, OR any address on our corporate domain
# (covers teammates whose mailbox isn't enrolled — the screenshot's "Received" bug).
outbound = from_email in own or (from_email.rsplit("@", 1)[-1] in our_domains if "@" in from_email else False)
direction = "Sent" if outbound else "Received"
gist = _summarize_email_gist(r["subject"], r["body_text"] or r["snippet"] or "")
if not gist:
continue # leave unproposed; a later pass retries once the model answers
note = _activity_note_text(r["sent_at"], direction, gist)
note = _activity_note_text(r["sent_at"], outbound, gist, _sender_display(r["from_name"], r["from_email"]), inv_name)
conn.execute(
"INSERT OR IGNORE INTO email_activity_proposals "
"(id,email_id,investor_id,investor_name,direction,summary,proposed_note,"