diff --git a/src/vm_web.py b/src/vm_web.py
index d6b092f..fe0cea2 100644
--- a/src/vm_web.py
+++ b/src/vm_web.py
@@ -304,24 +304,73 @@ def logout(vm_session: str = Cookie(default=None)):
@app.get("/", response_class=HTMLResponse)
-def index(vm_session: str = Cookie(default=None), msg: str = ""):
+def index(vm_session: str = Cookie(default=None), msg: str = "",
+ q: str = "", date_from: str = "", date_to: str = "", unread: str = ""):
mb = require(vm_session)
boxes = vm_auth.parse_mailboxes()
name = boxes.get(mb, {}).get("name", "")
con = vm_store.connect()
- rows = con.execute(
- "SELECT * FROM messages WHERE mailbox=? ORDER BY origtime DESC, id DESC",
- (mb,)).fetchall()
+
+ where = ["mailbox = ?"]
+ params = [mb]
+ if q:
+ like = "%" + q.replace("%", "%%") + "%"
+ where.append("(contact_name LIKE ? OR callerid LIKE ? OR summary LIKE ? OR transcript LIKE ?)")
+ params.extend([like, like, like, like])
+ if date_from:
+ where.append("origtime >= ?")
+ params.append(int(date_from))
+ if date_to:
+ where.append("origtime < ?")
+ params.append(int(date_to) + 86400)
+ if unread:
+ where.append("is_read = 0")
+
+ sql = ("SELECT * FROM messages WHERE " + " AND ".join(where) +
+ " ORDER BY origtime DESC, id DESC")
+ rows = con.execute(sql, params).fetchall()
con.close()
- banner = '
%s
' % escape(msg) if msg else ""
- if not rows:
- return page("Messages", banner + ''
- 'No voicemails yet.New messages appear '
- 'here automatically once transcribed.
',
- mb, name)
+ # filter bar
+ filters = ""
+ if q or date_from or date_to or unread:
+ parts = []
+ if q:
+ parts.append("search: %s " % escape(q))
+ if date_from:
+ parts.append("from: %s " % escape(date_from))
+ if date_to:
+ parts.append("to: %s " % escape(date_to))
+ if unread:
+ parts.append("unread only")
+ filters = (''
+ % (" · ".join(parts), BASE))
+
+ banner = ""
+ if msg:
+ banner = '%s
' % escape(msg)
+ if not rows:
+ return page("Messages", banner + filters +
+ ''
+ 'No voicemails match your filters.'
+ 'Try clearing the search or adjusting the date range. '
+ '
', mb, name)
+
+ out = [banner, filters,
+ '' % (BASE, escape(q or ""), escape(date_from), escape(date_to),
+ " checked" if unread else "")]
- out = [banner]
for r in rows:
tags = "".join('%s ' % escape(t)
for t in json.loads(r["intents"] or "[]"))
@@ -345,6 +394,14 @@ def index(vm_session: str = Cookie(default=None), msg: str = ""):
'%s
'
% escape(r["transcript"]))
+ who_raw = r["contact_name"] or r["callerid"] or "Unknown caller"
+ num = vm_contacts.extract_number(r["callerid"])
+ if num and r.get("contact_name"):
+ who = ('%s '
+ % (BASE, num, escape(who_raw)))
+ else:
+ who = escape(who_raw)
+
out.append("""
%s
%s%s
@@ -359,7 +416,7 @@ def index(vm_session: str = Cookie(default=None), msg: str = ""):
%s
""" % (
"" if r["is_read"] else " unread",
- escape(r["contact_name"] or r["callerid"] or "Unknown caller"),
+ who,
escape(fmt_time(r["origtime"])),
(" · " + escape(fmt_dur(r["duration"]))) if r["duration"] else "",
escape(r["summary"] or "(no speech detected)"),
@@ -368,9 +425,8 @@ def index(vm_session: str = Cookie(default=None), msg: str = ""):
BASE, r["id"], "Mark unread" if r["is_read"] else "Mark read",
BASE, r["id"], r["id"],
('+ Add to contacts '
- % (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 ""))
+ % (num or "", (r["contact_name"] or "").strip() or ""))
+ if not r["contact_name"] and num else ""))
return page("Messages", "".join(out), mb, name)
@@ -526,37 +582,44 @@ def _contact_form(action, c=None, msg=""):
@app.get("/contacts", response_class=HTMLResponse)
-def contacts_list(vm_session: str = Cookie(default=None), msg: str = ""):
+def contacts_list(vm_session: str = Cookie(default=None), msg: str = "", q: str = ""):
mb = require(vm_session)
con = vm_store.connect()
- total = con.execute("SELECT COUNT(*) c FROM contacts").fetchone()["c"]
- rows = con.execute(
- "SELECT id, number_e164, name, email, updated_at FROM contacts "
- "ORDER BY name ASC, id DESC"
- ).fetchall()
+
+ if q:
+ like = "%" + q.replace("%", "%%") + "%"
+ rows = con.execute(
+ "SELECT id, number_e164, name, email, updated_at FROM contacts "
+ "WHERE name LIKE ? OR number_e164 LIKE ? OR email LIKE ? "
+ "ORDER BY name ASC, id DESC",
+ (like, like, like),
+ ).fetchall()
+ else:
+ rows = con.execute(
+ "SELECT id, number_e164, name, email, updated_at FROM contacts "
+ "ORDER BY name ASC, id DESC"
+ ).fetchall()
+ total = len(rows)
con.close()
banner = '%s
' % escape(msg) if msg else ""
if not rows:
return page("Contacts", banner + ''
- 'No contacts yet. Use the button below to import from your '
- 'address book, or add them one by one.
'
+ 'No contacts yet.'
'', mb)
- out = [banner, ''
- '
' % (total, BASE, BASE, total),
- '
' % BASE,
- '
']
+ out = [banner, ''
+ '%d contacts
'
+ '
' % (
+ BASE, escape(q or ""),
+ ('Clear ' % BASE) if q else "",
+ total)]
for r in rows:
num = escape(r.get("number_e164") or "")
@@ -577,6 +640,74 @@ def contacts_list(vm_session: str = Cookie(default=None), msg: str = ""):
return page("Contacts", "".join(out), mb)
+@app.get("/contacts/history/{num}", response_class=HTMLResponse)
+def contact_history(num: str, vm_session: str = Cookie(default=None), msg: str = ""):
+ mb = require(vm_session)
+ normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(num)) or num
+ con = vm_store.connect()
+
+ rows = con.execute(
+ "SELECT * FROM messages WHERE mailbox=? AND callerid LIKE ? "
+ "ORDER BY origtime DESC, id DESC",
+ (mb, "%%%s%%" % normalized),
+ ).fetchall()
+
+ c = con.execute(
+ "SELECT id, name, email FROM contacts WHERE number_e164 = ? LIMIT 1",
+ (normalized,),
+ ).fetchone()
+ con.close()
+
+ who = escape(c["name"]) if c and c.get("name") else escape(num)
+ title = "History: %s" % who
+
+ banner = ""
+ if msg:
+ banner = '%s
' % escape(msg)
+
+ if not rows:
+ return page(title, banner + ''
+ 'No voicemails from this caller yet.
', mb)
+
+ out = [banner, '' % (BASE, len(rows), who)]
+
+ for r in rows:
+ tags = "".join('%s ' % escape(t)
+ for t in json.loads(r["intents"] or "[]"))
+ audio = ""
+ if r["audio_sha"]:
+ audio = (' '
+ % (BASE, r["id"]))
+ out.append("""
+
%s
+
%s%s
+
%s
+%s
+%s
+%s
+
""" % (
+ "" if r["is_read"] else " unread",
+ who,
+ escape(fmt_time(r["origtime"])),
+ (" · " + escape(fmt_dur(r["duration"]))) if r["duration"] else "",
+ escape(r["summary"] or "(no speech detected)"),
+ ('%s
' % tags) if tags else "",
+ audio, ("Full transcript "
+ '%s
' % escape(r["transcript"]))
+ if r["transcript"] else "",
+ BASE, r["id"], "Mark unread" if r["is_read"] else "Mark read",
+ BASE, r["id"], r["id"]))
+
+ return page(title, "".join(out), mb)
+
+
@app.get("/contacts/new", response_class=HTMLResponse)
def contact_new(vm_session: str = Cookie(default=None)):
require(vm_session)