From b09255473a5b91ff5bdf418ad5f61107b941dd6b Mon Sep 17 00:00:00 2001 From: jp Date: Thu, 13 Aug 2026 13:23:10 +0100 Subject: [PATCH] JS delete + add-contact modal + MySQL contacts table - vm_web.py: deleteVoicemail() XHR removes .card node on success - vm_web.py: openAddContact() modal with name/number/email fields - vm_web.py: POST /add_contact adds/updates MySQL contacts + backfills messages - show '+ Add to contacts' button when caller not in contacts - contacts.conf: backends=mysql,file - vm_import_contacts.py: VCF -> MySQL contacts import (527 rows) - Apache CSP: script-src 'self' 'unsafe-inline' for inline JS handlers - contacts table created in asterisk MySQL DB (number_e164 UNIQUE) --- src/vm_web.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/src/vm_web.py b/src/vm_web.py index 1ed51f2..d2f1abe 100644 --- a/src/vm_web.py +++ b/src/vm_web.py @@ -26,6 +26,7 @@ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from html import escape import vm_auth +import vm_contacts import vm_store SESSION_HOURS = int(os.environ.get("VM_SESSION_HOURS", "12")) @@ -158,6 +159,19 @@ border-radius:8px;margin-bottom:12px;font-size:14px} border-radius:8px;margin-bottom:12px;font-size:14px} table{width:100%;border-collapse:collapse} td{padding:6px 0;vertical-align:top} +.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.45); + align-items:center;justify-content:center;z-index:1000} +.modal-bg.open{display:flex} +.modal{background:#fff;border-radius:12px;padding:22px 24px;max-width:420px; + width:92%;box-shadow:0 6px 24px rgba(0,0,0,.25)} +.modal h3{margin:0 0 12px;font-size:16px} +.modal label{font-weight:600;font-size:14px;margin:10px 0 4px;display:block} +.modal input{width:100%;padding:10px 12px;border:1px solid #d4dcea; + border-radius:8px;font:15px inherit;background:#fff} +.modal .row{display:flex;gap:8px;margin-top:14px;justify-content:flex-end} +.add-btn{font-size:12px;padding:5px 10px;border-radius:6px;background:#eef3ff; + color:#2f6fed;border:1px solid #c5d4ff;cursor:pointer;font-weight:600} +.add-btn:hover{background:#dce6ff} """ @@ -170,11 +184,60 @@ def page(title, body, mailbox=None, name=None): %s

✉ Voicemail

%s%s
-
%s
""" % ( +
%s
+ + + + +""" % ( escape(title), CSS, ('%s · mailbox %s' % (escape(name or ""), escape(mailbox))) if mailbox else "", - nav, body)) + nav, body, BASE, BASE)) def fmt_time(ts): @@ -291,9 +354,8 @@ def index(vm_session: str = Cookie(default=None), msg: str = ""):
Download -
-
+ + %s
""" % ( "" if r["is_read"] else " unread", escape(r["contact_name"] or r["callerid"] or "Unknown caller"), @@ -303,7 +365,11 @@ def index(vm_session: str = Cookie(default=None), msg: str = ""): ('
%s
' % tags) if tags else "", callback, audio, transcript, BASE, r["id"], "Mark unread" if r["is_read"] else "Mark read", - BASE, r["id"], BASE, r["id"])) + BASE, r["id"], r["id"], + ('' + % (vm_contacts.extract_number(r["callerid"]) or "", + (r["contact_name"] or "").strip() or "")) + if not r["contact_name"] and vm_contacts.extract_number(r["callerid"]) else "")) return page("Messages", "".join(out), mb, name) @@ -425,3 +491,42 @@ def healthz(): n = con.execute("SELECT COUNT(*) c FROM messages").fetchone()["c"] con.close() return {"ok": True, "messages": n} + + +@app.post("/add_contact") +async def add_contact(request: Request, vm_session: str = Cookie(default=None)): + """Add/update a contact via the web portal (authenticated users only).""" + mb = require(vm_session) + form = await request.form() + num = (form.get("number") or "").strip() + name = (form.get("name") or "").strip() + email = (form.get("email") or "").strip() or None + if not name: + return RedirectResponse(BASE + "/?msg=Name+is+required", status_code=303) + + normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(num)) if num else None + + con = vm_store.connect() + if normalized: + row = con.execute( + "SELECT id FROM contacts WHERE number_e164 = ?", (normalized,) + ).fetchone() + if row: + con.execute( + "UPDATE contacts SET name=?, email=?, updated_at=NOW() WHERE id=?", + (name, email, row["id"]), + ) + else: + con.execute( + "INSERT INTO contacts (number_e164, name, email) VALUES (?, ?, ?)", + (normalized, name, email), + ) + # If a voicemail row already has this number, backfill the name. + con.execute( + "UPDATE messages SET contact_name=?, contact_email=? " + "WHERE callerid LIKE ? AND (contact_name IS NULL OR contact_name = '')", + (name, email, "%%%s%%" % normalized), + ) + con.commit() + con.close() + return RedirectResponse(BASE + "/?msg=Contact+saved", status_code=303)