Add side-by-side selected-contacts panel and simplify contact rows to name + company

Selected panel now mirrors the contact list's width and stays in sync via a
pageshow reset and refreshSelectedList(). Contact rows drop the raw email/
conference/draft-opened clutter in favor of just name and company. Also
sorts the contact list alphabetically and clears out test-drafting noise
that had accumulated in the dash's Outreach Log during today's debugging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-08-17 08:04:55 +02:00
1 parent b745d6f15d
commit 2ac38607c7
3 files changed
+66 -37

No files matched your search

@@ -6,11 +6,12 @@
{% if error %}<div class="warning">{{ error }}</div>{% endif %}
<form method="post">
<div class="card">
<div style="display:flex; gap:20px; align-items:flex-start;">
<div class="card" style="flex:1; min-width:0;">
<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>
<button type="button" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=true); refreshSelectedList();">Select All</button>
<button type="button" class="secondary" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=false); refreshSelectedList();">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;">
@@ -19,12 +20,10 @@
{% for c in contacts %}
{% 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 %}>
<div class="contact-row {% if c.sent %}disabled{% endif %}" data-name="{{ (c.first ~ ' ' ~ c.last)|lower }}" data-email="{{ c.email }}" {% 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 %}
{% if c.sent %}<span class="meta">(already marked as sent)</span>{% endif %}
<span>{{ c.first }} {{ c.last }}</span>
{% if c.company %}<span class="meta">&mdash; {{ c.company }}</span>{% endif %}
</div>
{% else %}
<p class="sub">No contacts in the sheet yet.</p>
@@ -34,6 +33,13 @@
<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" style="flex:1; min-width:0;">
<h2>Selected <span class="sub" style="font-weight:400;" id="selected-count">(0)</span></h2>
<div class="contact-list-scroll" id="selected-list">
<p class="sub">No one selected yet.</p>
</div>
</div>
</div>
<div class="card">
<h2>Message template</h2>
@@ -64,7 +70,9 @@
</form>
<script>
// Prevent double-clicks/double-submits from firing the same action twice (e.g. writing
// two draft files or opening two Mail.app windows for one click).
// two draft files or opening two Mail.app windows for one click). Reset on pageshow so a
// browser back-button restore (bfcache) doesn't leave every button permanently disabled
// from whatever was clicked before navigating away.
document.querySelector("form").addEventListener("submit", function (e) {
if (e.target.dataset.submitted) {
e.preventDefault();
@@ -76,6 +84,14 @@
});
});
window.addEventListener("pageshow", function () {
var form = document.querySelector("form");
delete form.dataset.submitted;
document.querySelectorAll("form button[type=submit]").forEach(function (btn) {
btn.disabled = false;
});
});
var preselected = document.getElementById("preselected-contact");
if (preselected) { preselected.scrollIntoView({ block: "center" }); }
@@ -85,5 +101,33 @@
row.classList.toggle("no-match", q !== "" && row.dataset.name.indexOf(q) === -1);
});
});
// Mirrors exactly who's currently checked, using the same row markup as the contact
// list itself, so there's never a mismatch between "what it says" and "who's selected".
function refreshSelectedList() {
var selectedList = document.getElementById("selected-list");
var selectedCount = document.getElementById("selected-count");
var checked = Array.from(document.querySelectorAll(".contact-check:checked"));
selectedCount.textContent = "(" + checked.length + ")";
if (!checked.length) {
selectedList.innerHTML = "<p class=\"sub\">No one selected yet.</p>";
return;
}
selectedList.innerHTML = "";
checked.forEach(function (cb) {
var row = cb.closest(".contact-row");
var clone = row.cloneNode(true);
clone.removeAttribute("id");
var checkbox = clone.querySelector("input[type=checkbox]");
if (checkbox) { checkbox.remove(); }
selectedList.appendChild(clone);
});
}
document.querySelectorAll(".contact-check").forEach(function (cb) {
cb.addEventListener("change", refreshSelectedList);
});
refreshSelectedList();
</script>
{% endblock %}
+12 -10
View File
@@ -41,19 +41,21 @@ def get_contacts():
except Exception:
return []
contacts = []
def clean(value):
text = str(value if value is not None else "").strip()
return "" if text.lower() == "nan" else text
for _, row in df.iterrows():
conference = str(row.get("Conference", "")).strip()
conference = "" if conference.lower() == "nan" else conference
draft_opened = str(row.get("Draft Opened", "")).strip()
draft_opened = "" if draft_opened.lower() == "nan" else draft_opened
contacts.append({
"first": row.get("First Name", ""),
"last": row.get("Last Name", ""),
"email": row.get("Email", ""),
"conference": conference,
"sent": str(row.get("Materials Sent", "")).strip().lower() == "yes",
"draft_opened": draft_opened,
"first": clean(row.get("First Name", "")),
"last": clean(row.get("Last Name", "")),
"email": clean(row.get("Email", "")),
"company": clean(row.get("Company", "")),
"conference": clean(row.get("Conference", "")),
"sent": clean(row.get("Materials Sent", "")).lower() == "yes",
"draft_opened": clean(row.get("Draft Opened", "")),
})
contacts.sort(key=lambda c: (c["first"].lower(), c["last"].lower()))
return contacts
+1 -18
View File
@@ -3820,24 +3820,7 @@
"time": "3:40 PM - 5:10 PM"
}
],
"outreach_log": [
{
"date": "15.08.26",
"note": "Test note - confirming the bridge works"
},
{
"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"
}
],
"outreach_log": [],
"notes": "",
"value_tags": [
"money"