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 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.
|
- `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 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/
|
npm run build # outputs dist/
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy the static build to the web root (Apache serves it and proxies `/api/`
|
Deploy the **`dist/` output** (not the repo root) to the web root — the root
|
||||||
to :8098 and `/audio/` to :8099):
|
`index.html` is the Vite *dev* entry (`<script src="/src/main.tsx">`) and would
|
||||||
|
render a blank page in production:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -A rsync -a --exclude node_modules --exclude dist \
|
sudo rsync -a --delete dist/ /home/txt3/domains/vm.txt3.net/public_html/
|
||||||
/home/jp/Work/voicemail-ui/ /home/txt3/domains/vm.txt3.net/public_html/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep this repo's copy in sync for documentation/commit purposes:
|
Keep this repo's copy in sync for documentation/commit purposes:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function ContactHistory() {
|
|||||||
</div>
|
</div>
|
||||||
{isLoading && <div className="card empty">Loading...</div>}
|
{isLoading && <div className="card empty">Loading...</div>}
|
||||||
{error && <div className="card err">{(error as Error).message}</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 key={m.id} className={"card" + (m.is_read ? "" : " unread")}>
|
||||||
<div className="who">{data.contact}</div>
|
<div className="who">{data.contact}</div>
|
||||||
<div className="meta">{m.time}{m.duration_fmt ? ` · ${m.duration_fmt}` : ""}</div>
|
<div className="meta">{m.time}{m.duration_fmt ? ` · ${m.duration_fmt}` : ""}</div>
|
||||||
|
|||||||
@ -38,23 +38,23 @@ export default function ContactsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((c) => (
|
{data.map((c: any) => (
|
||||||
<tr key={c.id}>
|
<tr key={c.id}>
|
||||||
<td>
|
<td style={{ minWidth: 0 }}>
|
||||||
<b>{c.name || "Unnamed"}</b><br />
|
<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}`}
|
{c.number_e164} {c.email && `· ${c.email}`}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ textAlign: "right", whiteSpace: "nowrap" }}>
|
<td style={{ textAlign: "right", verticalAlign: "middle" }}>
|
||||||
<a className="btn" href={`/contacts/edit/${c.id}`}>Edit</a>
|
<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>
|
<a className="btn" href={`/contacts/history/${encodeURIComponent(c.number_e164 || c.name)}`}>History</a>
|
||||||
{" "}
|
<DeleteBtn id={c.id} name={c.name} />
|
||||||
<DeleteBtn id={c.id} name={c.name} />
|
<span style={{ position: "relative", display: "inline-block" }}>
|
||||||
<span style={{ position: "relative", display: "inline-block", marginLeft: 4 }}>
|
<ContactMenu number={c.number_e164} name={c.name} />
|
||||||
<ContactMenu number={c.number_e164} name={c.name} />
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -121,7 +121,7 @@ function ContactMenu({ number, name }: { number?: string; name?: string }) {
|
|||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
className="btn"
|
className="btn"
|
||||||
href={"https://who-called.co.uk/Number/" + lookup}
|
href={"https://www.google.com/search?q=uk+number+" + lookup}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ display: "block", borderRadius: 0, border: "none", width: "100%", justifyContent: "flex-start" }}
|
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>}
|
{isLoading && <div className="card empty">Loading...</div>}
|
||||||
{error && <div className="card err">{(error as Error).message}</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 whoRaw = m.contact_name || m.callerid || "Unknown caller";
|
||||||
const num = sanitiseNumber(m.callerid);
|
const num = sanitiseNumber(m.callerid);
|
||||||
const showAdd = !m.contact_name && num;
|
const showAdd = !m.contact_name && num;
|
||||||
@ -192,7 +192,7 @@ function MsgContactMenu({ number, name, callerid }: { number?: string; name?: st
|
|||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
className="btn"
|
className="btn"
|
||||||
href={"https://who-called.co.uk/Number/" + lookup}
|
href={"https://www.google.com/search?q=uk+number+" + lookup}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ display: "block", borderRadius: 0, border: "none", width: "100%", justifyContent: "flex-start" }}
|
style={{ display: "block", borderRadius: 0, border: "none", width: "100%", justifyContent: "flex-start" }}
|
||||||
|
|||||||
@ -332,6 +332,12 @@ td {
|
|||||||
padding: 10px 6px;
|
padding: 10px 6px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
border-bottom: 1px solid var(--border);
|
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); }
|
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}
|
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