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.
This commit is contained in:
beantownbabe committed 2026-08-16 09:48:12 +02:00
1 parent b0360b6b68
commit b745d6f15d
9 files changed
+80 -78

No files matched your search

+1
View File
@@ -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
@@ -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
@@ -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,
@@ -365,38 +365,44 @@ a { color: var(--accent-strong); }
<section>
<h2>What the mail actually looks like</h2>
<p class="section-lede">A real draft generated from <code>conference_contacts.xlsx</code>, using the <code>wavegate_focus</code> template &mdash; this is exactly what sits in <code>output_drafts/</code> today, unedited.</p>
<p class="section-lede">A real draft generated from <code>conference_contacts.xlsx</code>, using the <code>reconnect_message</code> template &mdash; one of two templates now available in the tool's dropdown (the other, <code>wavegate_focus</code>, stays available too).</p>
<div class="email-preview">
<div class="email-preview-header">
<div><span class="k">Subject:</span>Following up on Radiation-Hardness Testing &amp; Wavegate</div>
<div><span class="k">Subject:</span>Following up on TXServices &amp; Wavegate</div>
<div><span class="k">From:</span>mckinley@mckinleyblack.com</div>
<div><span class="k">To:</span>pgaal@txproducts.de</div>
</div>
<div class="email-preview-body">Hi Peter,
It was great meeting you at SPIE 2026. Following up on our conversation
about Radiation-Hardness Testing &amp; Wavegate on 25.08.26.
It was a pleasure meeting you at SPIE 2026 on 25.08.26.
Attached is more detail on how it speeds up beamline experiments.
I really enjoyed your session on Radiation-Hardness Testing &amp; Wavegate, 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&ndash;30 minute call in the coming weeks to explore potential collaboration.
Best regards,
Mckinley</div>
<div class="email-preview-attachments"><code>txservices.pdf</code><code>wavegate.pdf</code></div>
</div>
<div class="template-source-label">The template behind it &mdash; <code>message_templates/wavegate_focus.txt</code></div>
<pre class="template-source">SUBJECT: Following up on <span class="ph">{{Topic}}</span>
<div class="template-source-label">The template behind it &mdash; <code>message_templates/reconnect_message.txt</code></div>
<pre class="template-source">SUBJECT: Following up on TXServices &amp; Wavegate
Hi <span class="ph">{{First Name}}</span>,
It was great meeting you at <span class="ph">{{Conference}}</span>. Following up on our conversation
about <span class="ph">{{Topic}}</span> on <span class="ph">{{Date Met}}</span>.
It was a pleasure meeting you at <span class="ph">{{Conference}}</span> on <span class="ph">{{Date Met}}</span>.
Attached is more detail on how it speeds up beamline experiments.
I really enjoyed your session on <span class="ph">{{Topic}}</span>, 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&ndash;30 minute call in the coming weeks to explore potential collaboration.
Best regards,
@@ -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;
@@ -7,15 +7,20 @@
<form method="post">
<div class="card">
<h2>Contacts</h2>
<h2>Contacts <span class="sub" style="font-weight:400;">({{ contacts|length }} total)</span></h2>
<div style="margin-bottom:10px;">
<button type="button" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=true)">Select All</button>
<button type="button" class="secondary" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=false)">Select None</button>
<a href="/" style="margin-left:8px; font-size:0.85rem;">Refresh from Excel</a>
</div>
<input type="text" id="contact-search" placeholder="Search by name..." style="margin-bottom:10px; max-width:300px;">
<div class="contact-list-scroll">
{% set ns = namespace(found=false) %}
{% for c in contacts %}
<div class="contact-row {% if c.sent %}disabled{% endif %}">
<input type="checkbox" class="contact-check" name="contacts" value="{{ c.email }}" {% if c.sent %}disabled{% endif %}>
{% set is_preselected = preselect_email and c.email|lower == preselect_email %}
{% if is_preselected %}{% set ns.found = true %}{% endif %}
<div class="contact-row {% if c.sent %}disabled{% endif %}" data-name="{{ (c.first ~ ' ' ~ c.last)|lower }}" {% if is_preselected %}id="preselected-contact" style="background:#eef4fd;"{% endif %}>
<input type="checkbox" class="contact-check" name="contacts" value="{{ c.email }}" {% if c.sent %}disabled{% endif %} {% if is_preselected %}checked{% endif %}>
<span>{{ c.first }} {{ c.last }} &lt;{{ c.email }}&gt;</span>
{% if c.conference %}<span class="meta">&mdash; {{ c.conference }}</span>{% endif %}
{% if c.draft_opened %}<span class="meta">(draft opened {{ c.draft_opened }})</span>{% endif %}
@@ -24,6 +29,10 @@
{% else %}
<p class="sub">No contacts in the sheet yet.</p>
{% endfor %}
</div>
{% if preselect_email and not ns.found %}
<p class="warning" style="margin-top:10px;">No contact with email {{ preselect_email }} found in the sheet yet &mdash; add them first.</p>
{% endif %}
</div>
<div class="card">
@@ -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);
});
});
</script>
{% endblock %}
+1 -53
View File
@@ -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")
+13 -5
View File
@@ -807,7 +807,7 @@
<button class="ab-filter-chip" data-source="Engaged Non-Follower">Engaged Non-Follower</button>
<button class="ab-filter-chip" data-source="Conference">SPIE 2026 Conference</button>
<button class="ab-filter-chip" id="ab-followers-link">Followers ↗</button>
<a class="ab-filter-chip" style="text-decoration:none;" href="address_book/conference_mail_merge/SPIE_2026_Presenter_Contacts.xlsx" download>Download Contact Sheet ↓</a>
<a class="ab-filter-chip" style="text-decoration:none;" href="address_book/conference_mail_merge/conference_contacts.xlsx" download>Download Contact Sheet ↓</a>
<a class="ab-filter-chip" style="text-decoration:none;" href="http://localhost:8505" target="_blank" rel="noopener" title="Opens the Mail Merge tool in a new tab. Must be running first (webapp.py in address_book/conference_mail_merge/).">Open Mail Merge Tool ↗</a>
</div>
<div class="ab-toolbar">
@@ -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("") + "</div>";
}
// 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 "<a class=\"conf-compose-email-btn\" href=\"" + href + "\" target=\"_blank\" rel=\"noopener\" title=\"Opens a ready-to-send Mail.app draft via the Mail Merge tool (must be running first).\">Compose Email</a>";
var href = "http://localhost:8505/?email=" + encodeURIComponent(p.email);
return "<a class=\"conf-compose-email-btn\" href=\"" + href + "\" target=\"_blank\" rel=\"noopener\" title=\"Opens the Mail Merge tool with this contact selected (must be running first).\">Compose Email</a>";
}
function renderConferences() {