Commit Graph
15 Commits
Author SHA1 Message Date
Claude c2e02f78ae asterisk vendor: always append TURN port even when TURN_SERVER is host-only
entrypoint.sh's turn_server fallback only appended :TURN_PORT when TURN_SERVER
was completely unset — a TURN_SERVER carried over from an older install (or
set to a bare host by hand) passed straight through with no port, so the
Sipnetic QR export's "st=turn:user:pass@host" field ended up missing the
port entirely. Append it whenever the configured value has no colon at all,
not just when it's empty.
2026-08-13 02:50:20 +00:00
Claude b2d064573f Move ai-stack and paintplus vendored source under vendor/
Matches the existing vendor/easy-asterisk convention (used by
services/asterisk.sh) instead of two one-off top-level directories that
cluttered the repo root and didn't look like anything else next to
setup.sh, lib/, services/, extras/. Only the two services' own SRC_DIR
path resolution and header comments needed updating — nothing else in
the repo referenced the old ./ai-stack / ./paintplus paths.

Also documents vendor/ in README.md's Layout section.
2026-08-04 12:55:09 +00:00
Claude 104fd26b6d Set SO_REUSEADDR on the web admin's TCPServer — the actual root cause
Live-confirmed the retry fix from the previous commit wasn't enough:
still "Address already in use" after 20s of retries (10 attempts,
2s backoff), only to succeed on its own sometime after that. That
delay pattern is TIME_WAIT, not a process-death race — and this code
was never going to avoid it, because socketserver.TCPServer defaults
allow_reuse_address to False. (http.server.HTTPServer sets this for
you; the plain base class used here does not.) Without SO_REUSEADDR,
the kernel can refuse to rebind a port with a lingering TIME_WAIT
socket from the previous instance for up to 60s, regardless of
whether that old process is even still alive — which is also why the
entrypoint.sh fix waiting for the process to exit didn't help either.

Set socketserver.TCPServer.allow_reuse_address = True before binding.
This is the standard fix for exactly this symptom. Keeping the retry
loop from the previous commit too, for the (now much smaller) window
where network_mode: host still has no Docker-managed port mapping to
instantly free.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 19:18:09 +00:00
Claude 4d2829f116 Wait for the web admin process to actually die on shutdown
Complements the retry fix on the bind side: pkill only sends SIGTERM
and returns immediately, it doesn't wait for the process to exit and
release its socket. Under network_mode: host there's no Docker-
managed port mapping to tear down, so the next container's bind
attempt was racing however long this process actually took to die —
sometimes still holding the port when the next container started.

Poll for it to actually exit (up to 2s), falling back to SIGKILL if
it's still lingering, before proceeding with the rest of shutdown.
With a clean handoff here, the web admin's own bind-retry (previous
commit) should rarely even need to kick in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 19:12:47 +00:00
Claude c42dc7e013 Retry web admin port bind instead of crashing on the first race
Live-confirmed: OSError: [Errno 98] Address already in use on the
web admin's TCPServer bind, right after a container recreate under
network_mode: host. Unlike bridge-mode port publishing, there's no
Docker-managed mapping to instantly free on teardown — the previous
container's own web admin process has to actually die first, and a
fast recreate-right-after-recreate can race that. The process crashed
immediately instead of retrying, so the web admin silently never came
up despite entrypoint.sh correctly launching it.

Retry the bind up to 10 times with a 2s backoff before giving up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 19:05:53 +00:00
Claude d9af3ecc3f Fix invalid "pjsip reload" command used everywhere (11 call sites)
"pjsip reload" was never a valid Asterisk CLI command — confirmed
live: `asterisk -rx "pjsip reload"` returns "No such command 'pjsip
reload'". The real command is "module reload res_pjsip.so".

Every reload-after-change call in the script used the invalid form,
both in the interactive CLI (add/edit/delete device, transport setup,
TLS cert sync) and in every web admin mutation (add_device,
delete_device, rename_device, change_device_category) — all silently
no-op'd, since `asterisk -rx` just prints its own "no such command"
error to a discarded/redirected output and returns normally either
way. Endpoints only ever picked up new pjsip.conf entries after a
full container restart (which re-reads config from scratch at
startup) or a manual `module reload res_pjsip.so` — never from the
web admin's own reload call, live-confirmed: a device stayed
Unavailable with zero registration attempts logged until a manual
reload picked it up immediately.

Global replace across all 11 occurrences.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:56:53 +00:00
Claude cc63d51d24 Default new devices to mobile category and TLS transport
Both defaulted to whatever sorted/listed first (kiosks category,
LAN/VPN UDP transport) — reasonable for a fixed intercom install, but
the common case here is adding a phone over the internet. Default the
category select to "mobile" specifically (not just first-in-list, so
it survives category reordering) and make FQDN/Internet (TLS) the
default transport option instead of LAN/VPN (UDP).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:20:41 +00:00
Claude 52db0fa921 Auto-start the web admin on every container start
The web admin script (/usr/local/bin/easy-asterisk-webadmin) is
generated on demand by the interactive CLI, but only ever lived in
the container's writable layer — not baked into the image, not
bind-mounted. Every docker compose down/up wiped it, and the
entrypoint's start logic only ran "if the file already exists", so
it silently never started again until someone manually ran the CLI's
Web Admin menu once per recreate.

Added a --write-web-admin-script non-interactive entry point
(same pattern as --rebuild-dialplan) and call it unconditionally
before the existence check, so the web admin comes back on its own
every time the container starts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:13:39 +00:00
Claude 24827a326f Show full SIP client config in web admin's device-created popup
The "device created" modal only ever showed extension/password/name,
so setting up a client (e.g. Sipnetic) meant hunting down the server
domain, port, and transport separately — and the password is only
ever shown this once, so re-checking it later isn't an option.

Now shows everything a SIP client needs in one place: display name,
server, port, transport, username, password, plus TURN/STUN details
when enabled. The backend reports the actual transport/port used
(the container always forces TLS/FQDN mode regardless of what's
selected in the form, so the frontend no longer has to guess).

Added "Copy All" and "Copy Password" buttons, with a document.
execCommand fallback for contexts where the Clipboard API isn't
available (e.g. plain-HTTP self-signed-cert access).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:00:28 +00:00
Claude 487b6446d7 Rebuild dialplan on every container start as a safety net
Devices/rooms trigger a dialplan rebuild themselves via the web admin
now, but that only fixes the problem going forward — endpoints added
before that fix (or by any future path that misses the call) stay
registrable-but-uncallable with no obvious cause until someone thinks
to run --rebuild-dialplan by hand.

Call it unconditionally once Asterisk is up, before the PJSIP
transport check. Cheap and idempotent — it just regenerates
extensions.conf from the current pjsip.conf/rooms.conf state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 17:35:08 +00:00
Claude 834436d101 Fix dialplan never rebuilding after web admin device/room changes
The web admin's device functions (add_device, delete_device,
rename_device, change_device_category) only ever called
"asterisk -rx pjsip reload" — they never regenerated
extensions.conf's [intercom] context, so newly added SIP endpoints
could register but could never call each other or dial into rooms
("extension not found in context 'intercom'").

The room functions (create_room, delete_room, rename_room,
update_room_members) already tried to fix this correctly by shelling
out to `easy-asterisk --rebuild-dialplan`, but that flag was never
actually wired up — main() at the bottom of the script ignores all
arguments and always launches the interactive menu, so every one of
those calls was a silent no-op too.

Fixed both: added real --rebuild-dialplan argument handling that
calls the existing rebuild_dialplan() bash function non-interactively,
and added the same subprocess call to the four device functions that
were missing it entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 17:25:35 +00:00
Claude 0035804b38 Fix false "TLS: NOT LOADED" startup banner in Easy Asterisk entrypoint
pjsip show transports was checked once, immediately after "core show
version" first responded — but res_pjsip can take a moment longer to
finish binding its transports, so the check would sometimes read an
empty transport list and print "NOT LOADED" even though transport-tls
came up correctly a second later (confirmed live: TLS SIP traffic on
5061 in the container logs right after the misleading banner).

Poll for up to 10s instead of checking once, matching the existing
core-show-version wait pattern further up the same script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 16:16:34 +00:00
Claude 225e277abb asterisk: default to FQDN mode, auto-detect VLANs, auto-sync Caddy certs
Defaults install_asterisk() to FQDN networking mode and prompts for VLAN/VPN
subnets (with host-network auto-detection to filter out noise like Docker
bridges) so phones on other networks get correct NAT/SDP handling from the
first boot.

The container now mounts Caddy's cert store read-only when Caddy is
installed, and the entrypoint syncs a matching Let's Encrypt cert for
DOMAIN_NAME automatically, re-checking every 12h to pick up renewals without
a restart. Falls back to self-signed only when no matching cert is found.

Also fixes a real bug hit in the field: a preserved/migrated pjsip.conf could
be missing the transport-udp/transport-tcp sections entirely, with no bind
error logged, silently blocking any device that registers without TLS. Adds
the same migration-injection already used for transport-tls.
2026-07-01 17:19:57 +00:00
Claude 7c3f101fe0 Add asterisk, nextcloud, onlyoffice, mattermost services + vendor/easy-asterisk
asterisk.sh (homelab):
- Easy Asterisk PBX with self-hosted coturn TURN server
- Vendored from outis1one/easy-asterisk v0.10.0 for offline install
- LAN-only or FQDN mode (TLS + TURN relay for remote access)
- Auto-answer SIP headers for intercom use case
- Authelia SSO for web admin; WEB_ADMIN_AUTH_DISABLED=true when chosen
- UFW rules: 5060-5061, 8080, 8088-8089, 3478, 10000-20000/udp, 49152-49252/udp
- Builds custom Docker image from vendor/easy-asterisk/

nextcloud.sh (utilities):
- Custom Dockerfile: nextcloud:apache + smbclient (SMB external storage)
- MariaDB 10.11 sidecar with matching env vars
- OVERWRITEPROTOCOL/OVERWRITECLIURL/TRUSTED_PROXIES set for Caddy
- Enables files_external app after first-run init (waits up to 90s)

onlyoffice.sh (utilities):
- JWT generated once, preserved across re-runs
- _ensure_yq: auto-installs yq v4 for FileBrowser config patching
- _wire_nextcloud: idempotent occ wiring (DocumentServerUrl, jwt_secret)
- _wire_filebrowser: patches config.yaml + restarts container
- Caddy block overrides X-Frame-Options to allow iframe embedding

mattermost.sh (utilities):
- PostgreSQL 15-alpine + Mattermost Team Edition + coturn (port 3479)
- 8443/udp for Calls plugin RTC server
- coturn uses --use-auth-secret HMAC mode (required by Calls plugin)
- SITE_URL computed from SITE_DOMAIN, promptable
- UFW: 8443/udp, 3479, 49153-49352/udp

vendor/easy-asterisk/:
- All upstream source files vendored for offline/self-contained installs
- Dockerfile, docker/entrypoint.sh, docker/coturn-entrypoint.sh
- easy-asterisk-v0.10.0.sh (6929-line management script)
- scripts/vpn-diagnostics.sh, scripts/dns-whitelist.sh
- .env.example

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-09 00:28:38 +00:00
Claude 0d81839c80 Vendor easy-asterisk source files; fix asterisk.sh and onlyoffice.sh
vendor/easy-asterisk/: All source files from outis1one/easy-asterisk v0.10.0
vendored so the repo is self-contained — no internet required at install time.
Includes the real Dockerfile (FROM ubuntu:24.04 + full Asterisk stack),
entrypoint.sh (IP detection, TLS cert gen, pjsip/rtp config, web admin),
coturn-entrypoint.sh (robust IP detection wrapper), and the management
script + diagnostic utilities.

services/asterisk.sh: Rewritten to copy from vendor/ instead of downloading
at runtime. Uses the upstream Dockerfile verbatim. Symlinks
easy-asterisk-v0.10.0.sh → easy-asterisk.sh for build context compatibility.

services/onlyoffice.sh: Complete rewrite with correct standalone bootstrap.
_ensure_yq() installs yq v4 automatically (arch-aware). JWT secret is
preserved across re-runs so rotating is explicit. _wire_nextcloud() and
_wire_filebrowser() run on every install invocation (idempotent), skipping
gracefully when containers aren't running rather than failing.

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-08 18:41:46 +00:00