Compare commits
3 Commits
b8c60605fd
...
acc84ad1ea
| Author | SHA1 | Date | |
|---|---|---|---|
| acc84ad1ea | |||
| dddb8c3663 | |||
| 14855eab73 |
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" }}>
|
||||
<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 }}>
|
||||
<ContactMenu number={c.number_e164} name={c.name} />
|
||||
</span>
|
||||
<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" }}>
|
||||
<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" }}
|
||||
|
||||
@ -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" }}
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user