Compare commits

..

3 Commits

Author SHA1 Message Date
jp
acc84ad1ea docs: fix frontend deploy instructions — rsync dist/, not the repo root
The previous deploy snippet rsync'd the UI repo root (excluding dist/),
which pushed the Vite DEV index.html (<script src=/src/main.tsx>) to
production and rendered a blank page. Correct to build then rsync the
dist/ output with --delete.
2026-08-13 20:45:49 +01:00
jp
dddb8c3663 frontend: prevent contacts content overflowing the viewport + sync UI repo
- ContactsPage: drop whiteSpace:nowrap on the action cell; wrap the
  Edit/History/Delete/⋮ buttons in a flex container with flex-wrap so they
  reflow instead of forcing the table wider than the viewport; allow the
  name/number/email cell to break long strings.
- index.css: td word-break:break-word + table max-width:100% as a guard.
- Sync the UI repo (ContactsPage/ContactHistory/MessagesPage implicit-any
  annotations that unblock npm run build) into the frontend/ mirror.
2026-08-13 20:42:29 +01:00
jp
14855eab73 Remove duplicated AddContactIn/add_contact/delete_all_contacts in vm_api.py
The contact create/delete endpoints and their Pydantic model were defined
twice (identical blocks). Python kept the second definition, so the first was
dead code. Drop the duplicate; module still compiles and each symbol is now
defined exactly once.
2026-08-13 20:33:44 +01:00
7 changed files with 29 additions and 71 deletions

View File

@ -52,7 +52,9 @@ Auth: voicemail `pin` from `voicemail.conf`. No separate portal passwords.
- Sudo requires password. Use `SUDO_ASKPASS=/home/jp/.hermes/askpass.sh` with `sudo -A <cmd>`.
- `sudo bash <script>` is blocked (nested root shell). Run privileged steps as individual `sudo -A <cmd>` calls.
- Deploy backend: `sudo -A cp /home/jp/asterisk-vm/vm_api.py /opt/vm-transcribe/vm_api.py && sudo -A systemctl restart vm-api`.
- Deploy frontend: `sudo -A rsync -a --exclude 'node_modules' --exclude 'dist' /home/jp/Work/voicemail-ui/ /home/txt3/domains/vm.txt3.net/public_html/`.
- Deploy frontend: build in the UI repo, then rsync the **`dist/`** output (not the repo root) to the web root:
`cd /home/jp/Work/voicemail-ui && npm install && npm run build && sudo rsync -a --delete dist/ /home/txt3/domains/vm.txt3.net/public_html/`.
NOTE: deploy `dist/`, never the repo root — the root `index.html` is the Vite *dev* entry (`<script src="/src/main.tsx">`) and will render a blank page in production.
---

View File

@ -201,12 +201,12 @@ npm install
npm run build # outputs dist/
```
Deploy the static build to the web root (Apache serves it and proxies `/api/`
to :8098 and `/audio/` to :8099):
Deploy the **`dist/` output** (not the repo root) to the web root — the root
`index.html` is the Vite *dev* entry (`<script src="/src/main.tsx">`) and would
render a blank page in production:
```bash
sudo -A rsync -a --exclude node_modules --exclude dist \
/home/jp/Work/voicemail-ui/ /home/txt3/domains/vm.txt3.net/public_html/
sudo rsync -a --delete dist/ /home/txt3/domains/vm.txt3.net/public_html/
```
Keep this repo's copy in sync for documentation/commit purposes:

View File

@ -15,7 +15,7 @@ export default function ContactHistory() {
</div>
{isLoading && <div className="card empty">Loading...</div>}
{error && <div className="card err">{(error as Error).message}</div>}
{data?.messages?.map((m) => (
{data?.messages?.map((m: any) => (
<div key={m.id} className={"card" + (m.is_read ? "" : " unread")}>
<div className="who">{data.contact}</div>
<div className="meta">{m.time}{m.duration_fmt ? ` · ${m.duration_fmt}` : ""}</div>

View File

@ -38,23 +38,23 @@ export default function ContactsPage() {
</div>
<table>
<tbody>
{data.map((c) => (
{data.map((c: any) => (
<tr key={c.id}>
<td>
<td style={{ minWidth: 0 }}>
<b>{c.name || "Unnamed"}</b><br />
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>
<span style={{ color: "var(--text-muted)", fontSize: 13, overflowWrap: "anywhere", wordBreak: "break-word" }}>
{c.number_e164} {c.email && `· ${c.email}`}
</span>
</td>
<td style={{ textAlign: "right", whiteSpace: "nowrap" }}>
<td style={{ textAlign: "right", verticalAlign: "middle" }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, justifyContent: "flex-end" }}>
<a className="btn" href={`/contacts/edit/${c.id}`}>Edit</a>
{" "}
<a className="btn" href={`/contacts/history/${encodeURIComponent(c.number_e164 || c.name)}`}>History</a>
{" "}
<DeleteBtn id={c.id} name={c.name} />
<span style={{ position: "relative", display: "inline-block", marginLeft: 4 }}>
<span style={{ position: "relative", display: "inline-block" }}>
<ContactMenu number={c.number_e164} name={c.name} />
</span>
</div>
</td>
</tr>
))}
@ -121,7 +121,7 @@ function ContactMenu({ number, name }: { number?: string; name?: string }) {
)}
<a
className="btn"
href={"https://who-called.co.uk/Number/" + lookup}
href={"https://www.google.com/search?q=uk+number+" + lookup}
target="_blank"
rel="noreferrer"
style={{ display: "block", borderRadius: 0, border: "none", width: "100%", justifyContent: "flex-start" }}

View File

@ -70,7 +70,7 @@ export default function MessagesPage() {
{isLoading && <div className="card empty">Loading...</div>}
{error && <div className="card err">{(error as Error).message}</div>}
{data?.messages?.map((m) => {
{data?.messages?.map((m: any) => {
const whoRaw = m.contact_name || m.callerid || "Unknown caller";
const num = sanitiseNumber(m.callerid);
const showAdd = !m.contact_name && num;
@ -192,7 +192,7 @@ function MsgContactMenu({ number, name, callerid }: { number?: string; name?: st
)}
<a
className="btn"
href={"https://who-called.co.uk/Number/" + lookup}
href={"https://www.google.com/search?q=uk+number+" + lookup}
target="_blank"
rel="noreferrer"
style={{ display: "block", borderRadius: 0, border: "none", width: "100%", justifyContent: "flex-start" }}

View File

@ -332,6 +332,12 @@ td {
padding: 10px 6px;
vertical-align: top;
border-bottom: 1px solid var(--border);
word-break: break-word;
}
/* Keep the contacts table (and any table) from forcing horizontal overflow */
table {
max-width: 100%;
}
tr:hover td { background: rgba(255, 255, 255, 0.01); }

View File

@ -582,53 +582,3 @@ def delete_all_contacts(vm_session: str = Cookie(default=None)):
return {"ok": True}
class AddContactIn(BaseModel):
number: str = ""
name: str
email: str | None = None
@app.post("/api/add_contact")
async def add_contact(body: AddContactIn, vm_session: str = Cookie(default=None)):
mb = _current_user(vm_session)[0]
num = (body.number or "").strip()
name = (body.name or "").strip()
email = (body.email or "").strip() or None
if not name:
raise HTTPException(400, "Name is required")
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 and row.get("id"):
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),
)
con.execute(
"UPDATE messages SET contact_name=?, contact_email=? "
"WHERE (REPLACE(REPLACE(callerid, ' ', ''), '\"', '') LIKE ? "
" OR REPLACE(REPLACE(callerid, ' ', ''), '\"', '') LIKE ?) "
"AND (contact_name IS NULL OR contact_name = '')",
(name, email, "%%%s%%" % normalized, "%%%s%%" % normalized.replace("+44", "0"),),
)
con.commit()
con.close()
return {"ok": True}
@app.post("/api/contacts/delete_all")
def delete_all_contacts(vm_session: str = Cookie(default=None)):
_current_user(vm_session)
con = vm_store.connect()
con.execute("DELETE FROM contacts")
con.commit()
con.close()
return {"ok": True}