Commit Graph
758 Commits
Author SHA1 Message Date
Claude 9390382bf4 Add Sipnetic QR-scan provisioning and a kiosk client installer download
Two concrete asks: "a QR code generator for sipnetic... displayed on the
security website" and a real download for the baresip kiosk client
instead of just pointing at docs.

Sipnetic QR: ea_device_sipnetic_string() builds Sipnetic's own documented
account-string format (n=/u=/d=/p=/dt=, semicolon-separated, from
https://www.sipnetic.com/qr-codes) from the exact same data the existing
device-details panel already reads back out of pjsip.conf -- unlike
ea_device_provisioning()'s deliberately generic plain-text file (written
because no vendor XML format could be verified), this one has real
documentation to build against. Rendered client-side with
davidshimjs/qrcodejs (MIT, wraps Kazuhiko Arase's original QRCode for
JavaScript) embedded verbatim with its license header intact -- no CDN
call, no new Python dependency, same self-contained approach as the rest
of this page. New "Sipnetic QR code" button on each extension's detail
panel; verified the embedded library actually renders (not just parses)
via a real jsdom run producing real QR cell output.

Kiosk installer download: _secdash_copy_kiosk_installer copies
vendor/easy-asterisk/easy-asterisk-v0.10.0.sh alongside the deployed app
at install time (both update and fresh-install paths) and a new
/download/kiosk-client-installer.sh route serves it -- linked directly
from the Ring Groups card's help text instead of only being reachable by
manually finding the file in the repo. Copied at install time rather
than read live, since a standalone run of this one file (no full repo
clone -- explicitly supported here) has no vendor/ directory to read
from; missing source degrades to a clean 404 on that one link, not an
install failure.

Verified: bash -n, py_compile on the extracted embedded app.py, node
--check on the extracted embedded JS (including the newly-embedded QR
library), and the account-string output checked directly against
Sipnetic's own documented example format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-29 03:30:13 +00:00
Claude 646b7b6fde Let an existing Ring Group's type/timeout be edited, not just set at creation
Rename, member add/remove, and DID assignment could all already be
changed after a room existed -- type and timeout could only be set once,
at creation, with no way to flip an existing Ring group to Page (or back)
without deleting and recreating it, losing its members/DID assignment in
the process. Reported directly: "I can't edit the ring group to change
it to a page group."

Turns the Timeout/Type columns into inline-editable controls (matching
the same select/input the creation form already uses) with a Save button
per row, backed by a new ea_update_room_settings() that rewrites just
those two fields in rooms.conf, leaving name/members/DID untouched --
same read-modify-write pattern ea_rename_room() already uses.

Verified: bash -n, py_compile on the extracted embedded app.py, node
--check on the extracted embedded JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-29 03:03:15 +00:00
Claude ca750f5432 Document Ring vs Page and auto-answer kiosks, in the dashboard itself
The user asked for the security webpage to actually explain how to set
these up, not just this conversation. Adds a "How this works" disclosure
to the Ring Groups card (matching the existing help-block pattern already
used on Extensions and Personal numbers) covering:

- Ring vs Page's actual difference (first-answer-wins hunt group vs
  Asterisk signaling auto-answer via SIP headers).
- Mixing an auto-answering device with normally-ringing phones needs no
  new group type or per-member setting -- auto-answer lives in the
  device's own SIP client config, so a plain Ring group already dials
  everyone at once and an auto-answer-configured device just picks up
  faster than a human can.

New docs/kiosk-paging-setup.md walks through the concrete path for a
dedicated always-on auto-answer device: Easy Asterisk's own baresip-based
"kiosk" client, installed on a separate small Linux machine (not the
Asterisk server's Docker container -- the vendor script explicitly skips
baresip install when it detects Docker locally). Also notes plainly that
a browser/WebRTC auto-answer option doesn't exist on this server today
(no WSS transport configured) and would be new infrastructure work, not
a quick addition.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-28 23:57:42 +00:00
Outis b166170481 Merge pull request #256 from outis1one/claude/asterisk-digital-ocean-consolidate-9dsrse
Read domain/TURN from Easy Asterisk's own config, and offer a setting…
2026-07-28 16:23:01 -04:00
Claude eaca45c83c Read domain/TURN from Easy Asterisk's own config, and offer a settings download
The panel kept showing "no domain set" and "TURN not configured" on a box
that has both. The cause was reading the wrong file: I had it reading the
host-side .env, a 600 file needing an ACL grant, a ProtectSystem exception
and a systemd Environment= line to even locate — three things that each had
to be right, and weren't.

The vendored web admin gets this right because it reads
/etc/easy-asterisk/config, written by the container's own entrypoint. That is
mounted on the host inside ASTERISK_EA_CONFIG_DIR, a directory this dashboard
is already granted read access to for rooms.conf. So it now reads that first,
falls back to .env, and finally to `docker exec cat` of the same file — each
source only filling gaps the previous left. No new permissions, and the value
shown is what Asterisk is actually running with rather than what the
installer asked for.

Also adds the requested provisioning file: a "Download settings" link per
extension serving a plain text file with server, username, password, display
name, transport, port, SRTP, a ready-made SIP URI, and TURN server/user/
password. Deliberately not a vendor-specific format — Sipnetic, Linphone,
Zoiper and Groundwire each want a different one and none could be verified
from here, and a confidently-wrong .xml is worse than a file you can read. It
warns in-file when the extension is UDP-only, when the cert is self-signed,
and that it contains a password.

The panel gains the SIP URI and says when the address shown is the host IP
rather than a configured domain.

Fixes a JS syntax error introduced with that note: an apostrophe escaped for
Python's benefit left a bare quote inside a single-quoted JS string, which
broke the whole page script — the table rendered empty. Now checked properly
by extracting the script and running `node --check` over it, which catches
this class of fault directly instead of inferring it from missing elements.

Verified with only Easy Asterisk's config present and .env absent entirely —
the state that failed before: domain, TURN server, user and password all
resolve, the download serves with the right filename, and the page has no
errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-28 20:19:46 +00:00
Outis 632cdb1c1c Merge pull request #255 from outis1one/claude/asterisk-digital-ocean-consolidate-9dsrse
Claude/asterisk digital ocean consolidate 9dsrse
2026-07-28 16:13:10 -04:00
Claude 1b3e7a6110 Fix extensions created from the dashboard, and show details by their row
Three separate faults, reported together as "the old web admin made
extensions correctly and this doesn't".

1. Every write through ea_docker_write left pjsip.conf owned by root. `tee`
   runs as root inside the container while Asterisk runs as `asterisk` and
   expects to own its own config — the vendored admin's add_device() chowns
   it back immediately after writing, and services/asterisk.sh's device
   migration does too. This was the one writer in the project that didn't,
   and is the most likely reason an extension created here behaves
   differently from one created in the vendored admin. Now chowned after
   every write, with a scoped sudoers entry for it, and a warning logged if
   the chown itself fails rather than passing silently.

2. The dashboard's update path rewrote the systemd unit and then restarted
   the service without daemon-reload, so systemd kept running the cached
   unit. Any Environment= line added since the last FRESH install was written
   to disk and ignored — which is exactly how a box with DOMAIN_NAME and TURN
   both set in .env still reported "no domain set" and "TURN not configured".

3. The connection panel rendered at the top of the card, so on any table long
   enough to scroll, the answer appeared off-screen above the row that was
   clicked. It is now a table row injected directly beneath its own
   extension, toggled by the same button, and it survives the transport and
   password actions by reopening after the reload they trigger.

Also: LAN devices now get ice_support=yes when a TURN server is configured,
matching the vendored admin (a device given TURN credentials but no ICE can't
use the relay); the panel reports the device type, so a Mobile extension can
be confirmed as such; and an unreadable .env now says which of "path not
set", "file missing", "permission denied" or "TURN_SERVER empty" applies
instead of the flat "not configured" that covered all four.

Verified: the three .env failure states each produce their own message; the
detail row lands directly after its anchor, only one is ever open, it clears
on re-render and reopens rather than sticking closed; no duplicate or missing
element IDs and no JS errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-28 20:08:49 +00:00
Claude 4f216fc8bb Correct two docs left describing SMS as ntfy push, not AMI delivery
services/sms-inbound.sh was rebuilt to deliver texts into Asterisk over AMI,
landing in the softphone's own thread, and ntfy was dropped from that path
entirely. Two pieces of prose written against the earlier design survived and
now contradict the working implementation:

- docs/anveo-direct-setup-guide.md still ended its "native Messages app"
  section with "Codes arrive as ntfy push notifications instead". The point
  of that section — no SIP client can write into Android Messages or iOS
  Messages — is unchanged, but the place texts actually land is Sipnetic's
  message thread.
- services/pstn-trunk.sh's generated README claimed inbound SMS "doesn't
  touch Asterisk at all" and set up ntfy notifications. It is now the exact
  opposite: sms-inbound reads this service's pstn-personal-dids.conf and
  pstn-groups.conf to resolve DID ownership, the same files the inbound-voice
  ring logic uses, which also makes install order matter — noted there now.

Documentation only; no behaviour change, and services/sms-inbound.sh is not
touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-28 19:55:34 +00:00
Outis ec4585e418 Merge pull request #254 from outis1one/claude/asterisk-digital-ocean-consolidate-9dsrse
Claude/asterisk digital ocean consolidate 9dsrse
2026-07-28 15:47:44 -04:00
Claude 1f6c7cf349 Default new extensions to TLS, with transport behind an Advanced disclosure
Making the transport a visible, equally-weighted choice meant new users had
to know the answer before they could get one right — and the wrong answer
fails in the least diagnosable way available: a UDP-only endpoint doesn't
refuse a TLS registration, it ignores it, so the phone times out and nothing
is logged anywhere.

The add form now creates Remote/FQDN (TLS 5061) extensions without asking.
TLS is listed first in the markup so it is the default before any script
runs, and the JS no longer switches it to LAN on a box with no domain — that
box is not better served by UDP, it just needs the phone told to trust a
self-signed certificate, which the disclosure now says at the point of
choosing.

Transport and auto-answer moved behind an "Advanced…" toggle, leaving name,
extension and category as the whole form. The toggle resets on cancel and
after a successful add so it doesn't stay open across uses.

Verified in the browser on two fixtures — with and without DOMAIN_NAME set —
that the panel starts hidden, the transport reads fqdn untouched in both
cases, and each shows the right hint when opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-28 19:43:21 +00:00
Claude 11f48ed970 Show connection details per extension, and let them be edited
Adding an extension from the dashboard gave back a password and nothing else
— no server, no TURN credentials, and no indication of which transport the
endpoint had actually been written with. A correctly-created extension and
one that could never register looked identical.

Each row gets an info button opening the full set: SIP server, username,
password, transport and port, and the TURN server/user/password. The password
is read back from pjsip.conf rather than regenerated, so re-pairing a handset
no longer means deleting and recreating the extension. The same panel can
switch an extension between LAN (UDP 5060) and Remote/FQDN (TLS 5061) and
reset its password in place, keeping its category, room membership and PSTN
permissions.

That transport choice is the likeliest cause of a phone that looks right and
never registers: an endpoint written transport=transport-udp will not answer
a TLS registration and the phone just times out. The add form now defaults to
Remote/FQDN whenever DOMAIN_NAME is set, rather than always LAN.

Reading TURN details needs Asterisk's .env, which is chmod 600 — the
installer now grants the service user read on that one file via ACL and lists
it in the unit's ReadOnlyPaths, since ProtectSystem=strict would otherwise
hide it. The UI says so plainly if the file still isn't readable.

Also fixes a real gap: ea_add_device is a third, independent writer of
endpoint blocks alongside the two vendor paths that services/asterisk.sh
patches, and it was not emitting message_context=sip-messaging — so an
extension created from this dashboard silently had no internal SIP messaging
while one created from the vendor admin did.

Verified against a fixture: transport round-trips TLS↔LAN without
accumulating duplicate transport/media_encryption/ice_support keys, password
reset rewrites only the target extension's auth section, delete still works
after edits, and the browser shows the populated panel with the add form
defaulting to fqdn when a domain is configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-28 19:42:13 +00:00
Outis 119ad1007c Merge pull request #253 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Confirm inbound SMS→AMI working live; rule out outbound over SIP
2026-07-28 13:49:58 -04:00
Claude a74da52760 Confirm inbound SMS→AMI working live; rule out outbound over SIP
Inbound: a real text through the full path (Anveo webhook -> relay ->
AMI MessageSend -> Sipnetic) landed with a SIP 200 OK, confirmed live.
Removes the last "UNVERIFIED"/"believed correct" hedges from
sms-inbound.sh now that the AMI permission class and the
Destination-not-To fix are both proven, not just plausible.

Outbound over SIP: tested directly by sending a MESSAGE toward Anveo's
trunk (the mirror image of the inbound webhook). Anveo's SBC responded
501 Not Implemented -- a real, unambiguous rejection of the method
itself. Closes off this avenue for good, symmetric with inbound
SMS-over-SIP already being confirmed unavailable on this DID: neither
direction is offered on this account via SIP. Sending still has no
working path here until Anveo activates HTTP SMS-API access.

Also updates docs/anveo-direct-setup-guide.md's SMS section, which still
described the ntfy-based mechanism this session fully replaced with
AMI/Sipnetic delivery, and corrects its stale "not available on Direct"
sending claim with what's actually been confirmed: SIP MESSAGE outbound
is a dead end (501), the HTTP API is real but pending Anveo activation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 13:41:31 +00:00
Outis 1765958c4b Merge pull request #252 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Claude/asterisk caller id number y1b3ls
2026-07-27 08:56:54 -04:00
Claude 0135b2ed27 Use Destination, not To, for MessageSend's endpoint resolution
`manager show command MessageSend` (this box's own Asterisk, requested
live) documents Destination as the field that actually resolves an
outgoing message's endpoint/technology; To is documented as a
backward-compatible fallback for the destination when Destination is
omitted, and separately as just the outgoing SIP MESSAGE's To: header
content when Destination IS provided. Two live attempts using only To
(bare "pjsip:212", then domain-qualified "pjsip:212@domain") both
produced zero SIP wire traffic -- confirmed via `pjsip set logger on`
during a real delivery attempt against an actively-registered contact --
meaning that documented fallback path isn't actually wired up on this
Asterisk version regardless of what the docs promise.

Switched to Destination using the docs' own "endpoint" form: bare
"pjsip:<ext>", no domain, which resolves via the endpoint's default
aor/contact -- the same live, registered contact `pjsip show contacts`
already confirmed exists for this extension.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 12:45:03 +00:00
Claude 5b339fdc0d Try qualifying MessageSend's To with a domain -- To/From asymmetry
Live test: AMI MessageSend to a bare "pjsip:212" produced zero SIP wire
traffic (confirmed via `pjsip set logger on` during a real delivery
attempt with the target extension actively registered) -- Asterisk
never even tried reaching the registered contact, meaning the failure
was in URI resolution before anything got sent, not a rejection from
the softphone. From already carried a domain (SMS_DOMAIN); To didn't.
Testing whether that asymmetry was the actual cause.

Explicitly a live experiment, not a confirmed fix -- next test will
show whether this produces real SIP MESSAGE traffic in the logger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 04:01:52 +00:00
Outis 445b14403e Merge pull request #251 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Fix the actual cause of every "file does not exist": ProtectHome=true
2026-07-26 23:45:17 -04:00
Claude e48460d6f2 Fix the actual cause of every "file does not exist": ProtectHome=true
Chased this as an ACL-ordering problem for the last several commits, and
those fixes were real and worth keeping, but none of them could ever
have fixed this: ProtectHome=true in the systemd unit doesn't just
restrict permissions, it mounts an empty, invisible filesystem over
/home, /root, and /run/user for the whole unit. ASTERISK_CONFIG_DIR
lives under /root/docker/... (or /home/<user>/docker/... on a non-root
install), so the relay process could never see it regardless of any ACL
grant on the real filesystem underneath -- from inside the sandboxed
unit it genuinely doesn't exist, while a plain unsandboxed shell
(confirmed live: `sudo -u smsrelay cat pstn-personal-dids.conf` outside
systemd) reads the exact same path fine.

Fix: ProtectHome=read-only instead of true. Still stops this service
from writing into /home or /root -- all it should ever need is read --
it just stops hiding them outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:43:38 +00:00
Outis cf2d0604b9 Merge pull request #250 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Grant smsrelay's ACL access after the Asterisk restart, not before
2026-07-26 23:31:12 -04:00
Claude 733a583c6a Grant smsrelay's ACL access after the Asterisk restart, not before
Reproduced live: pstn-personal-dids.conf was confirmed readable by
smsrelay right after a manual ACL grant, then unreadable again
("does not exist" in the relay's log -- os.path.isfile() swallows the
PermissionError and just returns False) immediately after the very next
fresh install. The only thing that ran in between was this same
install's own Asterisk container restart (needed to pick up the new AMI
secret).

The ACL grant was sequenced BEFORE that restart. CLAUDE.md documents the
container's entrypoint re-chowning its mounted config directory on every
restart and says chown alone can't touch ACL entries -- true, but
apparently this image's entrypoint also chmods, and chmod recomputes a
directory's ACL mask entry, which can silently weaken a named-user grant
made before it even though the grant's ACL entry itself is untouched.

Fix: do the grant last, after the restart-or-not branch, so nothing left
in this install run can undo it. ensure_docker_dir_ownership() (chown
only, confirmed in lib/common.sh, no chmod) stays where it was --
chow doesn't need this ordering fix, only the ACL grant does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:27:14 +00:00
Outis 39c9f20fa6 Merge pull request #249 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Log rejected sms-inbound webhook requests instead of dropping silently
2026-07-26 23:22:18 -04:00
Claude e59f16556a Log rejected sms-inbound webhook requests instead of dropping silently
A token mismatch returned a bare 404 with no journal line at all, so
"nothing is happening" was indistinguishable from "no request ever
arrived" -- exactly what a stale provider URL looks like after a
RELAY_TOKEN rotation (every full reinstall generates a new one, which
invalidates whatever's still pasted into the DID's SMS tab until it's
updated). Now logs the request's source IP and path length -- never the
attempted path itself, since that's unauthenticated input from whoever
hit the port, no reason to trust or echo it into the journal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:04:37 +00:00
Outis d91143a8b3 Merge pull request #248 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Stop the old sms-inbound instance before a fresh reinstall, not after
2026-07-26 23:00:54 -04:00
Claude 9207daa433 Stop the old sms-inbound instance before a fresh reinstall, not after
The port-taken message on the last run ("Port 8093 was taken — the relay
will use 8094") was this service colliding with itself, not a real
conflict. Fresh-install never stopped the previous run before scanning
for a free port, so it always found its own earlier process still bound
to 8093, silently moved to 8094, and then `systemctl enable --now` was a
no-op against an already-active unit -- meaning the OLD process (holding
the OLD AMI secret and OLD relay token, from before this session's
settings.env fix) kept serving traffic while the freshly-written config
and port sat unused underneath it.

Fix: `systemctl stop sms-inbound` right before the port scan. Now the
scan only reports a real conflict from something else, and the box
should settle back on 8093 (or whatever's actually free) with the
process that's really running matching what was just configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:00:30 +00:00
Outis 8fbc1b8cb0 Merge pull request #247 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Fix sms-inbound crashing on every "update" re-run
2026-07-26 22:57:59 -04:00
Claude 868a903683 Fix sms-inbound crashing on every "update" re-run
Root cause of the "line 9: from: unbound variable" crash: SMS_FORWARD_URL
stores Anveo's own template placeholders literally ($[from]$, $[to]$,
$[message]$ -- text Anveo substitutes on its end, not ours). Written into
settings.env double-quoted, that landed in the file as
SMS_FORWARD_URL="...?from=$[from]$&...". Harmless to write, but the
update path `source`s this same file on every re-run, and bash reads
$[from] as legacy arithmetic expansion ($[...] == $((...))) even inside
double quotes -- a bare name in it means "look up variable from", which
is unset, and setup.sh runs under `set -uo pipefail`, so nounset kills
the whole installer before it gets anywhere near the AMI-diagnostics
code from the last two commits.

Fix: single-quote every value in the written settings.env. A source'd
single-quoted assignment never re-expands its contents, so this is safe
regardless of what SMS_FORWARD_URL (or anything else in that file) holds.

This only fixes future writes -- the box that hit this already has a
broken settings.env on disk from before this fix existed, and "update"
mode sources that file before it gets a chance to rewrite it, so it will
crash the same way one more time even after pulling this. Choosing
"full install" instead on the next run skips the source entirely and
regenerates the file correctly quoted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 02:56:40 +00:00
Outis cbbb587b7e Merge pull request #246 from outis1one/claude/asterisk-caller-id-number-y1b3ls
sms-inbound: log exactly why recipient resolution failed
2026-07-26 22:38:36 -04:00
Claude 3727f375b1 sms-inbound: log exactly why recipient resolution failed
First live test dropped an inbound text with only "no owner found for
this DID, dropped" in the journal -- not enough to tell a config-dir
path problem from an ACL/permission problem from a DID-normalization
mismatch from an actually-unassigned DID, without re-triggering a real
text each time. resolve_recipients() now prints which of those it hit:
config dir unset, file missing (which also silently covers "smsrelay
can't traverse/read config/asterisk" -- os.path.isfile() swallows
PermissionError and just returns False), normalized DID has no section,
section has no owner=, or a group owner has no current members.

Nothing added is more sensitive than what's already visible on the
dashboard itself (config dir path, a DID's own digits, the set of DIDs
that exist) -- never the message body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 02:36:00 +00:00
Outis 5328601b7a Merge pull request #245 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Rebuild sms-inbound to deliver texts into Asterisk via AMI, not ntfy
2026-07-26 22:07:06 -04:00
Claude 34638e8400 Rebuild sms-inbound to deliver texts into Asterisk via AMI, not ntfy
SMS-over-SIP is confirmed unavailable on this DID (the account's SMS tab
only offers "Forward to URL", no MESSAGE/INVITE option) so the diagnostic
dialplan from the previous commit is dead weight for delivery purposes,
though pstn-trunk.sh's message_context wiring stays since it's harmless
and costs nothing to leave in place.

Pivots the whole service: same proven HTTP-webhook front end (rate limit,
constant-time token check, message/from/to extraction), but the delivery
target changes from a push notification to a real SIP MESSAGE landing in
the extension's own softphone (Sipnetic), via Asterisk's Manager
Interface. "Direct mode" and all ntfy options are removed entirely per
instruction to fully replace ntfy, not keep both paths.

Recipient resolution reuses the exact on-disk formats
services/security-dashboard.sh already reads/writes: DID -> owner from
pstn-personal-dids.conf, and for a Ring-Group owner ("@Name"), that
group's current members from pstn-groups.conf -- so delivery always
reflects live DID ownership and live group membership, not a snapshot
from assignment time.

AMI access is scoped tightly: a dedicated "smsrelay" manager.conf user
with only the "message" permission class, bound to 127.0.0.1, secret
generated via generate_password. The service account also gets a POSIX
ACL grant (not chmod/group, which the container's entrypoint reverts on
every restart) for read-only access to the two config files above.

Flagged as UNVERIFIED in code comments and will need a live test: the
"message" AMI permission class name and MessageSend's To/From/Body
parameter names are believed correct from documentation but unconfirmed
against a real Asterisk instance. ami_deliver() logs every AMI response
in full specifically so the first real delivery attempt is
self-diagnosing if something here is wrong.

Verified before commit: bash -n on the full file, and py_compile on the
extracted relay.py (configparser/socket/hmac/http.server, all stdlib).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 01:58:54 +00:00
Outis be46674413 Merge pull request #244 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Fix inbound-calling regression from the SMS-over-SIP include, and the
2026-07-26 21:24:55 -04:00
Claude 1caae68c34 Fix inbound-calling regression from the SMS-over-SIP include, and the
missing SMS-URI print on update

Two real bugs from the previous commit, both confirmed live on an
actual box where inbound calling had been working:

1. Interleaving the new #include between the two existing trunk
   includes broke 'from-pstn-trunk' loading entirely (confirmed via
   the box's own dialplan-reload verification check going from
   passing to failing on the exact run that added the new include).
   Root cause of the underlying quirk still isn't understood — same
   as the original single-file-structure bug this file's own comments
   already document — so the fix is defensive rather than diagnosed:
   append the new include at the very end of the file instead of
   between two already-working ones, minimizing disturbance to
   whatever ordering/adjacency that loader is sensitive to.

2. When the same verification check fails, _pstn_ensure_live_includes
   now automatically falls back to a full container restart and
   re-checks, instead of just warning and leaving inbound calls
   broken until the operator finds this on their own — confirmed live
   this session that a restart (not just dialplan reload) reliably
   fixes a context that's genuinely new to the running process.

3. The SMS-over-SIP URI printout was appended only to
   install_pstn-trunk's fresh-install tail, which "update in place"
   never reaches (it returns early). Factored into
   _pstn_print_sms_over_sip_info and called from both paths, so it
   actually shows up on the far more common update-in-place run.
2026-07-27 01:22:13 +00:00
Outis 3b2ae2d344 Merge pull request #243 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Add SMS-over-SIP inbound (Anveo MESSAGE method), diagnostic-first
2026-07-26 21:15:11 -04:00
Claude f691181248 Add SMS-over-SIP inbound (Anveo MESSAGE method), diagnostic-first
Wires message_context=pstn-sms-inbound onto the trunk endpoint (same
pattern services/asterisk.sh already uses for internal SIP MESSAGE
texting — a dedicated context so an inbound MESSAGE doesn't fall back
to context=from-pstn-trunk, which already owns "_X." for INVITE-based
calls) and writes a new [pstn-sms-inbound] dialplan context.

That context deliberately only logs every plausible source for the
sender number, destination DID, and body (X-ANVEO-SMS-FROM/TO
headers, MESSAGE(from)/(to)/(body)) rather than attempting to parse
and route immediately — Anveo's own documentation for this feature
has already proven unreliable twice this session (a dead SMSbySMS
endpoint, and three mutually-contradicting docs pages for the general
API), so this waits for a real test message to confirm which fields
actually show up before building extraction/routing logic against
them.

The install summary now reads DOMAIN_NAME straight from Easy
Asterisk's own .env (the same FQDN already serving SIP for the voice
trunk) and prints the exact `smshandler@<domain>:5060` URI to paste
into the DID's SMS-over-SIP settings, plus how to watch for the first
test message in the Asterisk console.
2026-07-27 01:12:00 +00:00
Outis 5a5e14230b Merge pull request #242 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Fix the real cause of the persistent "1 extension edited" false positive
2026-07-26 19:20:41 -04:00
Claude c02b908570 Fix the real cause of the persistent "1 extension edited" false positive
The previous fix (trimming the Name comparison) was real but not the
actual culprit here. loadExtensions()'s fallback for a device with no
pstn-permissions.conf entry — the normal, default state for any
extension never granted PSTN access — set restrict: "none", which
isn't one of the dropdown's five valid values (full/restricted/
restricted-in/restricted-out/internal). The <select> silently fell
back to its first option ("full") while the underlying model kept
"none", so the mismatch reappeared identically on every load
regardless of caching, matching a persistent bug survives hard
refresh and a private window. Use "internal" instead, matching the
documented default and the value the dropdown actually supports.
2026-07-26 23:00:18 +00:00
Outis 55f613dc4b Merge pull request #241 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Fix persistent "1 extension edited" false positive on page load
2026-07-26 18:56:12 -04:00
Claude 88b4510383 Fix persistent "1 extension edited" false positive on page load
rowEdits() compared the Name field's trimmed DOM value against the
untrimmed model value — any device whose stored name has stray
leading/trailing whitespace (e.g. from a manual pjsip.conf edit)
permanently failed that comparison on every load, showing the
unsaved-changes bar with nothing actually changed. Trim both sides.
2026-07-26 22:50:37 +00:00
Outis 15ca8de452 Merge pull request #240 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Ring Groups: assign a personal DID right on the row, inherited by mem…
2026-07-26 17:47:50 -04:00
Claude 5d9f7dad4b Ring Groups: assign a personal DID right on the row, inherited by members
Adds a Personal number column directly on the Ring Groups table (same
underlying write as the Personal Numbers card's owner picker, just
surfaced where it's actually needed instead of requiring a separate
trip) and closes the gap that group ownership previously left open:
a DID assigned to a group only rang members on inbound calls, with no
way for members to actually use it outbound.

_reconcile_group_cid_members() now makes a Ring Group's DID double as
every current member's outbound Caller-ID override in
pstn-permissions.conf, wired into every path that can change either
side of that relationship: assigning/reassigning/removing the group's
DID, joining/leaving the group, renaming it (repoints
pstn-personal-dids.conf's owner instead of leaving a stale reference,
members' Caller-ID untouched since neither the DID nor membership
changed), and deleting it (unassigns the DID entirely, clearing it
from every remaining member).

An individual extension's own personal_did always wins over a group's
- a member only ever inherits the group's DID into an empty
personal_did, and only ever loses an inherited one that still matches
the group's current DID, so a real individual assignment is never
clobbered. Verified end-to-end (create/assign/join/leave/rename/
delete, plus the individual-wins precedence case) against a local
harness before committing, given how much the outbound Caller-ID path
already burned this session.
2026-07-26 20:31:00 +00:00
Outis e5cfdc7fb8 Merge pull request #239 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Add member checkboxes to Ring Group creation
2026-07-26 16:05:34 -04:00
Claude fa22779d61 Add member checkboxes to Ring Group creation
Rooms' original flow was always create-empty-then-add-members-one-at-
a-time in the table afterward; the merge kept that flow and dropped
Groups' actual "check boxes, name it, done" creation UX, which is what
was actually asked for. Add-form now has a member picker so a Ring
Group can be built with its full membership in one step; existing
groups still use the per-row add/remove control for later changes.
2026-07-26 19:49:22 +00:00
Outis 25a4bd6302 Merge pull request #238 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Claude/asterisk caller id number y1b3ls
2026-07-26 13:53:05 -04:00
Claude fafa9d9fee Merge Groups into Ring Groups (renamed from Rooms), drop standalone Groups
Rooms already had the one thing Groups lacked — a real, dialable Easy
Asterisk extension with live ring/page dialplan logic — while Groups'
only unique capability was personal-DID ownership. Rebuilding that
ownership mechanism a second time under the Groups name would have
meant duplicating Rooms' dialplan-generation code; instead Rooms
(renamed "Ring Groups" for clarity) gains personal-DID ownership, and
the standalone Groups card/API/backend is removed entirely.

pstn-trunk.sh's existing group-owned-personal-DID machinery
(pstn-personal-group-ring.sh, reading pstn-groups.conf by name) is
untouched — a new sync_room_group_mirror() keeps that file in step
with a Ring Group's live membership automatically on every
create/rename/delete/member change, so pstn-trunk.sh never needs to
learn rooms.conf's format. Room names now validate against the same
pattern write_group() requires, so a room can never end up with a
name that would silently fail to sync once assigned a DID.

The PSTN-restart nudge now also fires on a Ring Group edit, but only
when that specific group currently owns a personal DID — most Ring
Groups never do, and prompting on every ordinary membership tweak
would just be noise.

"Auto-answer for everyone" is the existing Page (intercom) room type,
not a new control. Messaging stays exactly what it already was:
strictly per-extension, unrelated to Ring Group/personal-DID
membership.
2026-07-26 16:35:52 +00:00
Claude a37cba242a Default the Security Dashboard to the Extensions tab
Security Log was the landing tab purely by markup order; Extensions
is what actually gets used day to day.
2026-07-26 16:18:04 +00:00
Outis 0903f814ac Merge pull request #237 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Remove Categories management from dashboard, keep Rooms
2026-07-25 22:15:29 -04:00
Claude e9ff9b5112 Remove Categories management from dashboard, keep Rooms
Categories was a whole CRUD system (its own file, its own card, its
own API routes) for something that only ever had one functional
effect: tagging a device "mobile" to enable RTP NAT-keepalive tuning,
plus a per-category auto-answer default. Replaced with a single
"Mobile/cellular device" checkbox directly on the add-extension form
and each extension row, writing the same underlying category string
Easy Asterisk's device format already expects ("mobile" or
"standard") without a separate category registry to manage. Removes
ea_list_categories/ea_create_category/ea_delete_category/
ea_rename_category, their /api/ea-categories* routes, the Categories
card, and the now-unused categories.conf sudoers grant.

Rooms stays as-is — unlike Categories, it's the only actually-dialable
ring/page group in this dashboard (a real Easy Asterisk extension that
rings or pages members live), which the dashboard-only Groups feature
cannot replace.
2026-07-25 20:33:58 +00:00
Outis 4be78aae4a Merge pull request #236 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Improve PSTN commit-changes UX, collapse Extensions, drop call-limit UI
2026-07-25 15:53:47 -04:00
Claude c477d11eb7 Improve PSTN commit-changes UX, collapse Extensions, drop call-limit UI
The "Commit changes (restart Asterisk)" button was already wired
correctly, but lived in a passive banner that was easy to miss after
a save, letting an admin keep testing against Asterisk's stale
AST_CONFIG() read without realizing a restart was needed. Every
PSTN-affecting save now also prompts immediately ("Restart Asterisk
now?"), once per logical action (a single save, or one whole batch),
with the banner kept as a fallback for "not now".

Also:
- Extensions card is now collapsible like the other cards, open by
  default.
- Concurrent-call caps card removed from the dashboard UI (the
  underlying dialplan cap and pstn-limits.conf are untouched, just no
  longer editable from this page).
- Categories/Rooms and Groups/Personal numbers are now visually
  grouped under section headers, with a note clarifying that a Room
  is a real dialable ring-group extension while a Group is a
  dashboard-only bulk-action convenience, since both being "named
  sets of extensions" invited exactly that confusion.
2026-07-25 19:40:20 +00:00
Outis 3300025500 Merge pull request #235 from outis1one/claude/asterisk-caller-id-number-y1b3ls
Revert unsafe from_user removal, use PAI/RPID for per-call outbound CID
2026-07-25 14:09:42 -04:00