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)
This commit is contained in:
jp
2026-08-13 13:23:10 +01:00
parent d2f1ca8f9d
commit b09255473a

View File

@ -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):
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s</title><style>%s</style></head><body>
<header><h1>&#9993; Voicemail</h1>%s<span class="sp"></span>%s</header>
<div class="wrap">%s</div></body></html>""" % (
<div class="wrap">%s</div>
<div id="addContactBg" class="modal-bg">
<form class="modal" method="post" action="%s/add_contact">
<h3>Add contact</h3>
<label>Name</label>
<input type="text" name="name" required autocomplete="name">
<label>Phone number</label>
<input type="text" name="number" autocomplete="tel">
<label>Email</label>
<input type="email" name="email" autocomplete="email">
<div class="row">
<button type="button" class="btn" onclick="closeAddContact()">Cancel</button>
<button type="submit" class="primary">Save contact</button>
</div>
</form>
</div>
<script>
function openAddContact(number, name){
var bg = document.getElementById('addContactBg');
var inputs = bg.querySelectorAll('input');
inputs[0].value = (name || '');
inputs[1].value = (number || '');
bg.classList.add('open');
inputs[(name ? 1 : 0)].focus();
}
function closeAddContact(){
document.getElementById('addContactBg').classList.remove('open');
}
document.getElementById('addContactBg').addEventListener('click', function(e){
if (e.target === this) closeAddContact();
});
function deleteVoicemail(id, el){
var xhr = new XMLHttpRequest();
xhr.open('POST', '%s/delete/' + id, true);
xhr.onload = function(){
if (xhr.status === 200 || xhr.status === 303){
var card = el.closest('.card');
if (card) card.remove();
} else {
alert('Delete failed: ' + (xhr.responseText || xhr.status));
}
};
xhr.onerror = function(){ alert('Network error during delete'); };
xhr.send();
}
</script>
</body></html>""" % (
escape(title), CSS,
('<span style="font-size:14px;color:#dce6ff">%s &middot; mailbox %s</span>'
% (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 = ""):
<div class="row">
<form method="post" action="%s/read/%d"><button>%s</button></form>
<a class="btn" href="%s/audio/%d?dl=1">Download</a>
<form method="post" action="%s/delete/%d"
onsubmit="return confirm('Delete this voicemail permanently?')">
<button class="danger">Delete</button></form>
<button type="button" class="danger" onclick="deleteVoicemail(%d, this)">Delete</button>
%s
</div></div>""" % (
"" 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 = ""):
('<div style="margin-top:8px">%s</div>' % 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"],
('<button type="button" class="add-btn" onclick="openAddContact(%r, %r)">+ Add to contacts</button>'
% (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)