Commit Graph
21 Commits
Author SHA1 Message Date
Claude 3ffcde7294 Fix gitea-github-sync.sh: stop mirroring refs/pull/* into Gitea/GitHub
Both sync directions used `git clone --bare` for the first clone and
`git push --mirror` for the push. A bare clone pulls in every ref the
remote advertises, refs/pull/*/head included — GitHub (and Gitea,
same behavior) exposes PR refs over the same smart-HTTP endpoint a
plain bare clone reads from. --mirror then pushes every local ref
verbatim, including those, and gets rejected: Gitea's server-side
hook (and GitHub's own PR-ref protection) reserves that namespace for
itself.

  remote: error: hook declined to update refs/pull/1/head
  ! [remote rejected] refs/pull/1/head (hook declined)

Fix: scope the initial clone (now git init --bare + fetch, unified
with the repeat-sync path instead of a separate git-clone branch) and
the push to an explicit refs/heads/*:refs/heads/* + refs/tags/*:refs/tags/*
refspec in both directions, matching the refspec discipline the fetch
side already had. Added a defensive cleanup (delete any
refs/pull/*, refs/merge-requests/*, refs/changes/* found in the local
bare mirror before pushing) so a repo synced before this fix
self-heals on its next run instead of tripping the same hook forever.
2026-08-31 18:51:53 +00:00
Claude 9bf49b5daa ai-stack: fix invalid YAML in generated docker-compose.yml healthchecks
Confirmed live: `docker compose pull` failed with "yaml: line 44, column
29: mapping values are not allowed in this context" during the "Starting
Stack" phase of local-ai-setup.sh. Root cause: two healthcheck blocks
(ollama, chromadb) crammed interval/timeout/retries onto one
semicolon-separated line —

    interval: 30s; timeout: 10s; retries: 5

— which isn't valid YAML; a scalar value can't contain a second `key:`
token like that unless quoted. Split each into three separate properly
indented keys, matching how every other multi-key block in this same file
is written.

Verified by generating the actual docker-compose.yml via the real heredoc
(same one docker-stack.md's variables would produce) and parsing the
result with PyYAML — line 44 is exactly the fixed `interval: 30s` line,
and the full file now parses as valid YAML.

Pre-existing bug in the vendored source, unrelated to this session's
earlier ai-stack.sh/local-ai-setup.sh changes (those only touched the
pull-models.sh heredoc and the cloud-provider prompt, both well before
this point in the install) — first surfaced now because this is the first
run in this session to actually reach the "Starting Stack" step rather
than stopping earlier.
2026-08-21 04:01:48 +00:00
Claude ac76ef5181 ai-stack: offer an optional vision-capable Ollama model, including moondream
None of local-ai-setup.sh's tier-selected models (CHAT_MODEL/CODE_MODEL/
EMBED_MODEL) can read an image — there was no way to get vision support out
of this stack at all before now. Added a numbered pick-list to the
generated pull-models.sh, right after the existing DeepSeek-R1 optional
pull, matching that same read -rp pattern:

  1) moondream            ~1.7 GB  by Moondream AI — tiny, built for
                                    CPU-only or weak/old-GPU hardware
  2) llava:7b             ~4.7 GB  general-purpose vision
  3) qwen2.5vl:7b         ~6 GB    stronger accuracy, more RAM/VRAM
  4) llama3.2-vision:11b  ~7.9 GB  heaviest of the four

moondream is the recommended default — sized for exactly the "6 vCPU, 8GB
RAM, no GPU" case this was asked for, unlike the other three which assume
real GPU/RAM headroom.

Verified by actually running the heredoc that generates pull-models.sh
(with EMBED_MODEL/CHAT_MODEL/CODE_MODEL stood in) and syntax-checking the
resulting output script, not just the source — the outer heredoc is
unquoted so $-escaping mistakes wouldn't show up as a bash -n failure on
local-ai-setup.sh itself, only on what it generates.

services/ai-stack.md gets a matching "Vision models" section (sizes, the
manual pull command, and how to point an app's OPENAI_MODEL at one).
laptop_full_setup.sh's separate, non-interactive pull-models.sh generator
is untouched — it's not invoked anywhere in this repo's own install flow
(only local-ai-setup.sh is, from install_ai-stack()), so it's out of
scope here.
2026-08-21 03:09:07 +00:00
Claude d33c4da775 gitea-github-sync: pin fetch refspec explicitly, stop silencing stderr
Confirmed live: a repo's local bare mirror clone got stuck on a stale
commit indefinitely even though every sync run reported [ok] — fetch
never failed, it just wasn't updating refs/heads/* the way this script
assumes. GitHub had the real current commit; the bare clone (and
therefore what got pushed to Gitea) stayed frozen on an old one.

`fetch --all --prune` trusts remote.origin.fetch as stored in the bare
repo's own git config rather than asserting what that mapping actually
is — if it drifted from the +refs/heads/*:refs/heads/* convention a
fresh `git clone --bare` sets up (for whatever reason — this specific
repo's local clone directory's history is unclear), fetch would
"successfully" land new commits somewhere this script never reads
(refs/remotes/origin/*) while refs/heads/* — the ref that actually gets
mirrored — never moves. Deleting and re-cloning the affected repo's
local directory fixed it immediately, consistent with a refspec-drift
theory, though the exact original cause wasn't pinned down further.

Now pins `+refs/heads/*:refs/heads/*` explicitly on every fetch instead
of relying on `--all` plus whatever's configured, in both sync
directions. Also stopped redirecting stderr to /dev/null on every
clone/fetch/push call — a real auth or network failure now shows up in
the log instead of a bare "Failed to X" with no reason, which is what
made this bug take three rounds of manual ls-remote/rev-parse forensics
across two machines to actually pin down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-18 02:20:27 +00:00
Claude bd6c5d445a gitea-github-sync: fix set -e aborting do_sync on the first successful repo
((pull_count++)) evaluates to the PRE-increment value — 0 on the very
first successful pull/push — and under this script's `set -euo pipefail`,
an arithmetic command evaluating to 0 counts as a failing command and
kills the script immediately. Confirmed live: a real, fully successful
GitHub -> Gitea pull (visible in sync.log as "PULL ... OK") still made
the whole run exit non-zero and get reported as "Sync run failed", purely
because it was the first repo to sync (0 -> 1). Any subsequent repo in
the same run would have been fine, but most real installs only have a
handful of repos, so this could look like sync is just broken.

Switched all three counters (pull_count, push_count, fail_count) to
assignment form (`count=$((count + 1))`), which always exits 0 regardless
of the resulting value. page++ elsewhere in the file starts at 1, not 0,
so it isn't affected by this and was left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:52:13 +00:00
Claude eae02da074 gitea-github-sync: show the actual HTTP response on a failed user check
"Failed to reach GitHub API. Check GITHUB_TOKEN." (and the equivalent
Gitea message) pointed at the token every time, even when the real cause
was something else entirely — confirmed live twice in one debugging
session: once a GitHub-side 503 outage, once a Gitea account locked
behind a must-change-password 403. Both times the fix was to run the
same curl by hand to see the actual status/response.

Fold that same probe into the script itself: on failure, re-request with
-i and print the HTTP status and response body directly, so the failure
mode (bad token vs. remote outage vs. account lock vs. network/DNS) is
visible immediately instead of requiring a manual curl round-trip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:48:19 +00:00
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