/api/me/voicemail lists, streams (Range, download) and marks read or
deletes through AMI VoicemailMove/VoicemailRemove, then rescans so MWI
and the list agree. /api/me/greetings takes the raw recording or file and
converts it with ffmpeg to 8 kHz PCM (media/audio.ts). My Phone gets a
layout with Home (new voicemails) and Voicemail (play marks read,
download, delete, record/upload greetings via MediaRecorder); the list
refetches on the live voicemail.new event. e2e m4-t3 checks MWI goes off
within 5 s after play and delete, and a browser-recorded greeting is what
callers hear.
Task: M4-T3
services/voicemail.ts rescans a mailbox on Asterisk's MessageWaiting event
(and every mailbox on AMI connect) instead of watching files, upserts
voicemail_messages by msg_id, marks vanished ones deleted, sends
voicemail.new to the owner only, and emails the wav. Delete-after-email
uses AMI VoicemailRemove, and only when the audio was attached. Asterisk
24 has VoicemailMove/VoicemailRemove, so 04 §5.6 drops the renumbering
plan. New VOICEMAIL_DIR setting; e2e m4-t2 checks the email arrives
within 10 s with a playable attachment (SMTP sink shared with m3-t4).
Task: M4-T2
/setup walks a fresh install to working calls: password (done by the
forced change), company, network (detect public IP, LAN IP from
/api/system/status, router checklist; restarts telephony because the
transports changed), email (skippable), voip.ms trunk (Next unlocks on
Registered), phone numbers, users (extension auto-numbered, temporary
passwords shown once), main number routing, emergency (default E911
caller ID required once a trunk exists), done. Steps 7-8 ship now
because the acceptance needs a user and a routed number; their M6
choices (everyone, IVR) come later. Admins land on /setup until it is
finished or skipped.
api: /api/setup/status and /complete, POST
/api/settings/network/detect-public-ip (ipify, then ifconfig.me),
GET /api/system/status (M3 subset), POST /api/system/asterisk/restart
(an AMI disconnect during a restart counts as success).
restartRequired clears on AMI connect once Asterisk's uptime shows it
started after the change. The first phone number becomes the default
outbound caller ID when none is set (found by the acceptance run:
calls otherwise left with the provider's caller ID).
Verified: scripts/e2e/m3-t5.mjs on a fresh database against the fake
voip.ms: wizard end to end, then an inbound call to the DID rings the
new user's softphone and an outbound call leaves as 13125550000 with
the DID in Remote-Party-ID. m1-t4 now passes through "Skip setup".
Task: M3-T5
Dialplan: sp-emergency-dial (911, 9911) first in sp-from-ext;
sp-emergency has no class, channel-limit or DND check, dials every
enabled trunk in name order with the E911 caller ID, and raises
UserEvent SpEmergency. Static sp-emergency-alert (_X! so "0" matches)
for the ring-out.
api: SpEmergency -> emergency_events row, live emergency.call (admins
and notify users only), email via nodemailer (SMTP from settings) and
an Originate to every notify extension, each outcome recorded. Test
button runs the same with TEST in front, records and dials nothing.
GET/PATCH /api/settings (smtp password write-only, references
checked, read-only system keys), SMTP test, /api/system/warnings.
Fix: clearing a nullable setting stored SQL NULL (now JSON null).
UI: Settings page (Email, Emergency with the 06 §6 notice), user
Emergency tab, dashboard warnings banner, red 911 banner.
Verified: generator/emergency.test.ts; trunk-test.ts dials 911 from a
caller with no class to the fake provider (E911 caller ID in RPID,
SpEmergency on AMI, alert plays on baresip 101); scripts/e2e/m3-t4.mjs
(SMTP sink receives the test email, 101 rings, banner shows).
nodemailer pinned to 10.0.11 (newest release over two weeks old).
Task: M3-T4
voip.ms-first trunk form with the portal checklist, POP server list
and Test button; passwords are write-only and encrypted. Registration
and IP-auth trunks render per 03 §5.3 (TCP/TLS add ;transport=). The
API refuses a server name that doesn't resolve and transports that are
off, because either makes Asterisk drop the trunk.
Live state: registration from PJSIPShowRegistrationsOutbound (re-read
on every Registry event: two sub-accounts on one server share
Username/Domain), reachability from PJSIPShowEndpoint and trunk
ContactStatus (static contacts aren't in PJSIPShowContacts). Trunk
state is admin-only. Dashboard gets a "Trunks up" card.
trunk-test.ts (in pnpm test:asterisk) runs a fake voip.ms (second
Asterisk): the good trunk registers, a wrong password is Rejected,
qualify reaches the provider; its recorded AMI drives the unit tests.
scripts/e2e/m3-t1.mjs does the same through the UI. The acceptance
check with a real voip.ms sub-account is still to do.
Tests: settle() now awaits an apply instead of a fixed sleep (flaked
under full-suite load).
Task: M3-T1
LiveState loads extension states and registered devices from Asterisk on
every AMI connect and follows ExtensionStatus/ContactStatus; the hub sends
a snapshot then deltas batched every 250 ms (devices for admins only),
pings every 30 s and closes unauthenticated sockets with 4401. The web UI
keeps one reconnecting socket in a zustand store, shows a Reconnecting
banner, a Dashboard (online, on a call, telephony engine) and status dots.
Fix: Asterisk 24 has no 'pjsip reload'; reload with
'module reload res_pjsip.so'. AMI errors now carry the CLI output.
Milestone check, automated: call-test.ts registers two baresip phones and
holds a 30 s call with RTP both ways (pnpm test:asterisk); the recorded
session drives the live-state tests. scripts/e2e/m2-live.mjs runs api +
Asterisk + phones + browser on a private Docker network: a new extension
is live in Asterisk in ~1.3 s, the dashboard shows 2/2 online and red dots
during the call.
Task: M2-T6
render() is pure and gives every file the header (bodies arrive per task).
apply() runs one at a time, debounces requestApply, writes changed files
atomically (mode 0640), keeps .prev, sends each needed reload once, skips
pjsip reload and sets restartRequired when transports change, and records
generator_runs. AMI connects on startup and every reconnect forces a full
apply. Admin routes: config apply/rollback and a masked generated-files view.
Task: M2-T2
argon2id passwords with a min length and a common-password list (SecLists,
MIT). Login failures are limited per IP and per email; must_change_password
blocks everything outside /api/auth/*. The first admin is created from env
on startup. pnpm dev derives DATABASE_URL from .env.
Task: M1-T3