Device-test round 2: 4 in-app fixes + Matrix intake cleanup (v0.1.0:99)
Grant's real-phone testing surfaced seven items; this lands six (the seventh, in-app camera card intake, is planned in docs/handoffs/in-app-card-intake-plan.md). CRM half — ships in the s9pk (v0.1.0:99): - Intake fuzzy match no longer over-indexes on generic firm words. _name_similarity now compares DISTINCTIVE tokens only (generic descriptors — "Investment Group", "Capital", "Family Office" — stripped via _GENERIC_ORG_WORDS) for both the difflib ratio and the Jaccard, so "Fortitude Investment Group" stops surfacing Aether/Russell while "Aether Capital" still surfaces "Aether Investment Group". +2 regression cases. - Mobile grid "Last contact"/staleness sort is reversible. SortSheet gains opt-in dir/onToggleDir; other surfaces (Contacts/Pipeline) are untouched. - Mobile "Edit investor" prefills a contact's saved email. GET /api/fundraising/state heals a blank grid pill email from the linked classic contact (fundraising_contacts.contact_id -> contacts.email), fill-only, by pill order then name; the next one-row save persists it. +test_grid_email_heal.py. - Mobile quick-log pencil icon renders. iOS collapses a sole, centered, attribute-only -sized flex-child <svg>; .quicklog-btn svg now gets explicit CSS width/height + flex:none (the pattern the working bottom-tab/sort-pill icons use). The v97 fix only changed color. Matrix intake bot — ships on the Spark (bot-only, NOT the s9pk): - Approve/reject now redacts the whole intake thread (card + ack + main-timeline nudge + the user's own photo/note), mirroring the email-review room; redact_thread takes the room as an arg and matches replies by m.thread OR m.in_reply_to (so the nudge clears). No more in-Matrix confirmation after a commit (the thread vanishing is the ack). Needs the bot to hold a redact/moderator power level in the intake room. - New one-time backend/matrix_intake/redact_intake.py clears the room's pre-existing backlog (dry-run default; --apply). Tests 42/42 green; frontend render-smoke green. Frontend fixes are inspection + render -smoke verified (on-device confirm pending); the bot redaction is live-smoke only.
This commit is contained in:
@@ -171,9 +171,13 @@ async def main():
|
||||
store.put(root, proposal) # commit failed — restore so the user can retry
|
||||
await say(room_id, f"⚠️ write failed, nothing committed: {exc}", root)
|
||||
return
|
||||
await say(room_id, f"✅ {summary}", root)
|
||||
# Committed → clear the whole thread (card + ack + nudge + the user's note/photo),
|
||||
# like the email-review room. The thread vanishing is the acknowledgment; a confirmation
|
||||
# reply would just keep it alive (and need redacting too). Needs the bot's redact/mod
|
||||
# power in the intake room to clear the user's own messages — else those linger.
|
||||
await redact_thread(room_id, root)
|
||||
elif action == "reject":
|
||||
await say(room_id, "🗑️ Discarded — nothing written.", root)
|
||||
await redact_thread(room_id, root)
|
||||
elif action == "edit":
|
||||
field, value = payload
|
||||
proposal = proposals.apply_edit(proposal, field, value)
|
||||
@@ -212,42 +216,49 @@ async def main():
|
||||
await say(room_id, "➕ OK — adding as a new investor:\n\n"
|
||||
+ proposals.render(updated), root)
|
||||
elif action == "reject":
|
||||
await say(room_id, "🗑️ Discarded — nothing written.", root)
|
||||
await redact_thread(room_id, root) # discard → clear the thread, like an approve
|
||||
else: # unrecognized — re-show the shortlist
|
||||
store.put(root, proposal)
|
||||
await say(room_id, "I didn't catch that.\n\n" + proposals.render_disambiguation(proposal), root)
|
||||
|
||||
async def redact_card(event_id):
|
||||
"""Redact one event (best-effort). Redacting our OWN message needs no special power;
|
||||
redacting someone else's reply needs the bot to hold a redact/mod power level."""
|
||||
async def redact_card(room_id, event_id):
|
||||
"""Redact one event in `room_id` (best-effort). Redacting our OWN message needs no special
|
||||
power; redacting someone else's message (a human reply, or the user's original card photo /
|
||||
intake note) needs the bot to hold a redact/mod power level in that room."""
|
||||
try:
|
||||
await client.room_redact(review_room, event_id, reason="proposal resolved")
|
||||
await client.room_redact(room_id, event_id, reason="proposal resolved")
|
||||
except Exception as exc:
|
||||
print(f"matrix-intake: could not redact {event_id}: {exc}", flush=True)
|
||||
|
||||
async def redact_thread(root):
|
||||
"""Clear a resolved thread: redact the card AND every reply under it, so the thread drops
|
||||
out of the threads view (not just the main timeline). The card is ours (always redactable);
|
||||
the human's yes/no reply needs the bot's redact/mod power — if it lacks power that redact
|
||||
just no-ops and the reply lingers. Finds replies by scanning recent room history for
|
||||
m.thread events pointing at this root (the triggering reply is already synced, so a
|
||||
backward scan from the current token includes it)."""
|
||||
await redact_card(root)
|
||||
async def redact_thread(room_id, root):
|
||||
"""Clear a resolved thread in `room_id`: redact the root AND every message that hangs off it
|
||||
— the m.thread children (cards/acks/human replies) AND the main-timeline **nudge** (a plain
|
||||
m.in_reply_to reply, not a thread child), so the thread drops out of both the threads view
|
||||
and the timeline. For email-review the root is the bot's card; for intake it's the USER'S
|
||||
own note/photo, so clearing it (and the human reply) needs the bot's redact/mod power in that
|
||||
room — without it those just no-op and linger. Replies are found by scanning recent history
|
||||
from the current sync token (the triggering reply is already synced, so a backward scan
|
||||
includes it)."""
|
||||
await redact_card(room_id, root)
|
||||
token = getattr(client, "next_batch", None)
|
||||
if not token:
|
||||
return
|
||||
try:
|
||||
scanned = 0
|
||||
for _ in range(MAX_THREAD_SCAN_PAGES):
|
||||
resp = await client.room_messages(review_room, start=token,
|
||||
resp = await client.room_messages(room_id, start=token,
|
||||
direction=MessageDirection.back, limit=100)
|
||||
chunk = getattr(resp, "chunk", None)
|
||||
if not chunk:
|
||||
break
|
||||
for ev in chunk:
|
||||
rel = ((getattr(ev, "source", None) or {}).get("content", {}) or {}).get("m.relates_to") or {}
|
||||
if rel.get("rel_type") == "m.thread" and rel.get("event_id") == root:
|
||||
await redact_card(ev.event_id)
|
||||
in_reply = (rel.get("m.in_reply_to") or {}).get("event_id")
|
||||
# A thread child carries event_id==root; the un-threaded nudge carries only
|
||||
# m.in_reply_to.event_id==root. Catch both so the thread AND its main-timeline
|
||||
# pointer clear together.
|
||||
if rel.get("event_id") == root or in_reply == root:
|
||||
await redact_card(room_id, ev.event_id)
|
||||
token = getattr(resp, "end", None)
|
||||
scanned += len(chunk)
|
||||
if not token or scanned > 1000:
|
||||
@@ -275,7 +286,7 @@ async def main():
|
||||
return
|
||||
# Success → clear the whole thread (card + replies). No confirmation: the thread
|
||||
# vanishing is the acknowledgment, and a confirmation reply would keep it alive.
|
||||
await redact_thread(root)
|
||||
await redact_thread(review_room, root)
|
||||
elif decision == "reject":
|
||||
email_threads.pop(root, None)
|
||||
try:
|
||||
@@ -284,7 +295,7 @@ async def main():
|
||||
email_threads[root] = item
|
||||
await say(room_id, email_proposals.frame(f"⚠️ couldn't dismiss it ({str(exc)[:200]}). Try again."), root)
|
||||
return
|
||||
await redact_thread(root)
|
||||
await redact_thread(review_room, root)
|
||||
else:
|
||||
try:
|
||||
new_note = await asyncio.to_thread(email_proposals.revise_note, item.get("note") or "", text)
|
||||
@@ -332,7 +343,7 @@ async def main():
|
||||
if not ev:
|
||||
continue
|
||||
try:
|
||||
await redact_thread(ev)
|
||||
await redact_thread(review_room, ev)
|
||||
await asyncio.to_thread(crm_client.mark_email_proposal_closed, it["id"])
|
||||
email_threads.pop(ev, None)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user