From b745d6f15d88de80a2827e0007d8d5b4e8072261 Mon Sep 17 00:00:00 2001 From: beantownbabe <156287695+beantownbabe@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:48:12 +0200 Subject: [PATCH] Unify contact sheet, add reconnect message template, and scale the contacts UI conference_contacts.xlsx is now the single working file behind both the dash's Download button and the mail-merge tool -- populated with all 139 SPIE 2026 presenters (not just ones with emails already found), so Peter can fill in emails as he meets people at the conference. Also adds a second message template (reconnect_message) alongside the existing wavegate_focus one, and a search/scroll UI for the contacts list now that it's no longer a handful of rows. --- .gitignore | 1 + .../SPIE_2026_Presenter_Contacts.xlsx | Bin 19388 -> 0 bytes .../message_templates/reconnect_message.txt | 17 ++++++ .../message_templates/wavegate_focus.txt | 11 ++-- .../rollout_presentation.html | 30 ++++++---- .../conference_mail_merge/templates/base.html | 2 + .../templates/index.html | 25 +++++++- address_book/conference_mail_merge/webapp.py | 54 +----------------- index.html | 18 ++++-- 9 files changed, 80 insertions(+), 78 deletions(-) delete mode 100644 address_book/conference_mail_merge/SPIE_2026_Presenter_Contacts.xlsx create mode 100644 address_book/conference_mail_merge/message_templates/reconnect_message.txt diff --git a/.gitignore b/.gitignore index b5bbd8e..e34a511 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ nextcloud_export/ *.env address_book/conference_mail_merge/conference_contacts.xlsx address_book/conference_mail_merge/output_drafts/ +~$*.xlsx __pycache__/ *.pyc .Rhistory diff --git a/address_book/conference_mail_merge/SPIE_2026_Presenter_Contacts.xlsx b/address_book/conference_mail_merge/SPIE_2026_Presenter_Contacts.xlsx deleted file mode 100644 index 73959e89c7e27d7c562cddf0f38975f58094d792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19388 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�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 WER4`55QvL$K^avwgOR=g0{tJ4UOaIC diff --git a/address_book/conference_mail_merge/message_templates/reconnect_message.txt b/address_book/conference_mail_merge/message_templates/reconnect_message.txt new file mode 100644 index 0000000..ba49df5 --- /dev/null +++ b/address_book/conference_mail_merge/message_templates/reconnect_message.txt @@ -0,0 +1,17 @@ +SUBJECT: Following up on TXServices & Wavegate + +Hi {{First Name}}, + +It was a pleasure meeting you at {{Conference}} on {{Date Met}}. + +I really enjoyed your session on {{Topic}}, I found it insightful and gave me several new perspectives. + +I'd love to stay in touch, as I believe there are opportunities where our expertise could complement each other and create value for both of us. + +I've attached a short overview of TXproducts, including our services and our flagship solution, WaveGate. + +If you're open to it, I'd be happy to schedule a 20–30 minute call in the coming weeks to explore potential collaboration. + +Best regards, + +Mckinley diff --git a/address_book/conference_mail_merge/message_templates/wavegate_focus.txt b/address_book/conference_mail_merge/message_templates/wavegate_focus.txt index f75ff8e..b6d3cf9 100644 --- a/address_book/conference_mail_merge/message_templates/wavegate_focus.txt +++ b/address_book/conference_mail_merge/message_templates/wavegate_focus.txt @@ -1,13 +1,14 @@ -SUBJECT: Following up on {{Topic}} +SUBJECT: Following up on TXServices & Wavegate Hi {{First Name}}, -It was great meeting you at {{Conference}}. Following up on our conversation -about {{Topic}} on {{Date Met}}. +It was great meeting you at {{Conference}} on {{Date Met}}. -Attached is more detail on how it speeds up beamline experiments. +Your session about {{Topic}} was very interesting, and I am following up +on the info material we talked about regarding TXproducts' services and +flagship product WaveGate. -Happy to set up a short call if useful. +Would love to dive deeper — let's find a time to talk. Best regards, diff --git a/address_book/conference_mail_merge/rollout_presentation.html b/address_book/conference_mail_merge/rollout_presentation.html index 04f6c48..9a689cb 100644 --- a/address_book/conference_mail_merge/rollout_presentation.html +++ b/address_book/conference_mail_merge/rollout_presentation.html @@ -365,38 +365,44 @@ a { color: var(--accent-strong); }

What the mail actually looks like

-

A real draft generated from conference_contacts.xlsx, using the wavegate_focus template — this is exactly what sits in output_drafts/ today, unedited.

+

A real draft generated from conference_contacts.xlsx, using the reconnect_message template — one of two templates now available in the tool's dropdown (the other, wavegate_focus, stays available too).

-
The template behind it — message_templates/wavegate_focus.txt
-
SUBJECT: Following up on {{Topic}}
+    
The template behind it — message_templates/reconnect_message.txt
+
SUBJECT: Following up on TXServices & Wavegate
 
 Hi {{First Name}},
 
-It was great meeting you at {{Conference}}. Following up on our conversation
-about {{Topic}} on {{Date Met}}.
+It was a pleasure meeting you at {{Conference}} on {{Date Met}}.
 
-Attached is more detail on how it speeds up beamline experiments.
+I really enjoyed your session on {{Topic}}, I found it insightful and gave me several new perspectives.
 
-Happy to set up a short call if useful.
+I'd love to stay in touch, as I believe there are opportunities where our expertise could complement each other and create value for both of us.
+
+I've attached a short overview of TXproducts, including our services and our flagship solution, WaveGate.
+
+If you're open to it, I'd be happy to schedule a 20–30 minute call in the coming weeks to explore potential collaboration.
 
 Best regards,
 
diff --git a/address_book/conference_mail_merge/templates/base.html b/address_book/conference_mail_merge/templates/base.html
index 75a9802..5bc0c1c 100644
--- a/address_book/conference_mail_merge/templates/base.html
+++ b/address_book/conference_mail_merge/templates/base.html
@@ -44,6 +44,8 @@
   .contact-row:last-child { border-bottom: none; }
   .contact-row .meta { color: #5b6a78; font-size: 0.85rem; }
   .contact-row.disabled { color: #b6bfc8; }
+  .contact-row.no-match { display: none; }
+  .contact-list-scroll { max-height: 420px; overflow-y: auto; padding-right: 4px; }
   select, input[type=text] {
     padding: 7px 9px;
     border: 1px solid #d3dbe4;
diff --git a/address_book/conference_mail_merge/templates/index.html b/address_book/conference_mail_merge/templates/index.html
index 9d373e2..f801d33 100644
--- a/address_book/conference_mail_merge/templates/index.html
+++ b/address_book/conference_mail_merge/templates/index.html
@@ -7,15 +7,20 @@
 
 
-

Contacts

+

Contacts ({{ contacts|length }} total)

Refresh from Excel
+ +
+ {% set ns = namespace(found=false) %} {% for c in contacts %} -
- + {% set is_preselected = preselect_email and c.email|lower == preselect_email %} + {% if is_preselected %}{% set ns.found = true %}{% endif %} +
+ {{ c.first }} {{ c.last }} <{{ c.email }}> {% if c.conference %}— {{ c.conference }}{% endif %} {% if c.draft_opened %}(draft opened {{ c.draft_opened }}){% endif %} @@ -24,6 +29,10 @@ {% else %}

No contacts in the sheet yet.

{% endfor %} +
+ {% if preselect_email and not ns.found %} +

No contact with email {{ preselect_email }} found in the sheet yet — add them first.

+ {% endif %}
@@ -66,5 +75,15 @@ btn.disabled = true; }); }); + + var preselected = document.getElementById("preselected-contact"); + if (preselected) { preselected.scrollIntoView({ block: "center" }); } + + document.getElementById("contact-search").addEventListener("input", function (e) { + var q = e.target.value.trim().toLowerCase(); + document.querySelectorAll(".contact-row[data-name]").forEach(function (row) { + row.classList.toggle("no-match", q !== "" && row.dataset.name.indexOf(q) === -1); + }); + }); {% endblock %} diff --git a/address_book/conference_mail_merge/webapp.py b/address_book/conference_mail_merge/webapp.py index 5bef562..b29a5ef 100644 --- a/address_book/conference_mail_merge/webapp.py +++ b/address_book/conference_mail_merge/webapp.py @@ -95,6 +95,7 @@ def index(): attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM, error=request.args.get("error"), + preselect_email=(request.args.get("email") or "").strip().lower(), ) @@ -195,59 +196,6 @@ def open_in_mail(): return render_template("results.html", lines=lines, warnings=warnings) -@app.route("/quick_open") -def quick_open(): - """One-click entry point from the Sales & Marketing dash's presenter cards -- opens a - real Mail.app draft for a single contact by email, using the default template and - attachments. Skips the checkbox/template-picker UI entirely.""" - email = request.args.get("email", "").strip() - template_name = request.args.get("template") or "wavegate_focus.txt" - - if not email: - return render_template("results.html", lines=[], warnings=["No email provided."]) - - try: - full_df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts") - except Exception as exc: - return render_template("results.html", lines=[], warnings=[f"Could not read contact sheet: {exc}"]) - - if "Draft Opened" not in full_df.columns: - full_df["Draft Opened"] = "" - - match_mask = full_df["Email"].astype(str).str.strip().str.lower() == email.lower() - if not match_mask.any(): - return render_template("results.html", lines=[], - warnings=[f"No contact with email {email} found in conference_contacts.xlsx yet -- add them there first."]) - - try: - rows = rows_for_selection([email], template_name) - except ValueError as exc: - return render_template("results.html", lines=[], warnings=[str(exc)]) - - default_attachments = [str(p) for p in DEFAULT_ATTACHMENTS if p.exists()] - - lines = [] - warnings = [] - opened_any = False - for row_dict, subject, body in rows: - if "[[MISSING:" in subject or "[[MISSING:" in body: - warnings.append(f"Missing field(s) for {row_dict['Email']} -- check before sending.") - try: - mm.open_in_mail_app(subject, body, row_dict["Email"], default_attachments, mm.DEFAULT_FROM) - lines.append(f"Opened in Mail.app: {row_dict['First Name']} {row_dict['Last Name']} <{row_dict['Email']}>") - timestamp = datetime.now().strftime("%d.%m.%y %H:%M") - full_df.loc[match_mask, "Draft Opened"] = timestamp - opened_any = True - mm.log_to_dash_outreach(row_dict["Email"], f"Draft opened in Mail.app ({template_name}), {timestamp}") - except RuntimeError as exc: - warnings.append(f"Could not open Mail.app draft for {row_dict['Email']}: {exc}") - - if opened_any: - full_df.to_excel(mm.CONTACTS_FILE, index=False, sheet_name="Contacts") - - return render_template("results.html", lines=lines, warnings=warnings) - - @app.route("/preview", methods=["POST"]) def preview(): selected = request.form.getlist("contacts") diff --git a/index.html b/index.html index 771a2fe..95cfc03 100644 --- a/index.html +++ b/index.html @@ -807,7 +807,7 @@ - Download Contact Sheet ↓ + Download Contact Sheet ↓ Open Mail Merge Tool ↗
@@ -3828,6 +3828,14 @@ { "date": "15.08.26", "note": "Draft opened in Mail.app (wavegate_focus.txt), 15.08.26 21:24" + }, + { + "date": "16.08.26", + "note": "Draft opened in Mail.app (wavegate_focus.txt), 16.08.26 06:10" + }, + { + "date": "16.08.26", + "note": "Draft opened in Mail.app (reconnect_message.txt), 16.08.26 08:44" } ], "notes": "", @@ -4686,12 +4694,12 @@ }).join("") + "
"; } - // Hands off to the real Mail Merge tool (localhost:8505), which does the actual template - // merge, attachments, and Mail.app draft creation server-side — the tool must be running. + // Hands off to the real Mail Merge tool (localhost:8505) with this contact pre-checked, + // so the template/attachments/from-address stay a deliberate choice, not an auto-pick. function buildComposeEmailButton(p) { if (!p.email) { return ""; } - var href = "http://localhost:8505/quick_open?email=" + encodeURIComponent(p.email); - return "Compose Email"; + var href = "http://localhost:8505/?email=" + encodeURIComponent(p.email); + return "Compose Email"; } function renderConferences() {