The real sky-cam repo's capture.sh already replaces MotionEye/any NVR
itself (plain ffmpeg RTSP frame-grab) - it and daily_sunrise_video.sh's
optional audio capture are the only places that touch a camera's RTSP
URL directly. Every other script (4-seasons, montage-mvt, year-end-join,
moon-track, moon-phase-monthly) only reads JPEGs/audio already on disk.
So the entire motionEye->Frigate transition is pointing CAM_RTSP_<cam>
at Frigate's go2rtc restream (rtsp://<frigate-host>:8554/<cam>) instead
of the camera directly - no upstream script changes needed. Replaces
the previous frigate-retime.sh/export-API approach, which solved a
problem (matching an arbitrary recording length to music duration) that
sky-cam's own 4-seasons.sh/montage-mvt.sh already handle via JPEG frame
counts.
Duplicates services/sky-cam.sh into a Frigate-backed variant that pulls
recordings via Frigate's export API instead of a JPEG image folder.
Includes a frigate-retime.sh helper that exports a coarse timelapse,
measures its actual duration with ffprobe, and re-encodes once with a
computed setpts factor to hit an exact target length (e.g. a Four
Seasons movement's runtime).
Group membership added by 'usermod -aG docker' (in require_docker) doesn't
apply to the shell that invoked sudo — only to new logins. Users had to
manually run 'newgrp docker' or reconnect SSH after every install. Since a
child process can't change its parent shell's group list directly, the
practical fix is to exec a fresh 'su - ' login shell at the end
of the guided flow, which re-reads /etc/group and lands the user back in
the same terminal with docker access already active.
Gated on: running via sudo (SUDO_USER set), interactive (not --unattended),
docker group exists and the user is actually a member, and stdin is a real
tty — so this never fires for scripted/explicit-service/piped invocations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Previously re-running the installer always overwrote config.yml and .env
from scratch, silently discarding any real camera credentials already on
disk. Now install_frigate parses an existing config.yml + .env (best-effort,
matching this installer's own output shape) and presents a numbered list
of detected cameras with a menu:
[1] Keep everything as-is (no changes at all)
[2] Backup existing config and start fresh
[3] Add more cameras (keep these)
[4] Remove cameras (choose numbers, or 'all'), then optionally add more
Implementation switches from building config.yml/.env as concatenated text
blocks inline in the collection loop to parallel CAM_* bash arrays
(name/ip/port/var-names/enabled/notify/substream-suffixes), so cameras can
be parsed, listed, removed, and re-rendered independently:
- _frigate_parse_existing: reads go2rtc streams + cameras: enabled/notifications
from config.yml, and credential values from .env, into the CAM_* arrays
- _frigate_review_existing: numbered menu, mutates arrays per choice
- _frigate_next_suffix_int: kept cameras retain their existing FRIGATE_RTSP_USER[N]
var names unchanged; new cameras get the next unused numeric suffix so
credentials never collide after removals
- _frigate_camera_wizard / _frigate_render_config: same prompts and output
shape as before, now array-driven so kept + new cameras render uniformly
- _frigate_backup_existing: copies config.yml/.env/docker-compose.yml to a
timestamped backup-YYYYMMDD-HHMMSS/ dir before any destructive rewrite
Verified with a 5-scenario test harness (fresh install, keep-as-is producing
byte-identical output, add-camera preserving existing credentials, remove-
by-number without var collisions, backup-and-fresh) before committing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Previously Frigate always wrote a placeholder config.yml the operator had
to hand-edit to add cameras. Now install_frigate prompts to add cameras
one at a time (name, RTSP IP/port/user/password/path, optional sub-stream,
enabled, notifications), matching the go2rtc + cameras structure used in
production frigate configs:
- Each camera gets a go2rtc stream entry (+ optional _sub for detection)
and a cameras: block with ffmpeg inputs/roles, detect, notifications
- RTSP credentials/IPs are written to .env as FRIGATE_* variables (first
camera gets FRIGATE_RTSP_USER/PASSWORD, later cameras get numbered
suffixes _1, _2, ... to avoid collisions) and referenced in config.yml
via Frigate's {FRIGATE_VAR} substitution syntax — secrets never appear
in the YAML directly
- docker-compose.yml now includes env_file: .env so those vars actually
reach the container for substitution to work
- Skipping all camera prompts falls back to the original starter
config.yml for manual editing, preserving existing behavior
- README and DRY-RUN summary updated to reflect the new flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
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.
The 'Install Caddy now?' prompt ran regardless of the just-answered
Caddy location question, so choosing 'remote' still asked whether to
install Caddy locally — contradicting the choice made one prompt earlier.
Gate it on CADDY_MODE being local (or unset, for configs predating the
wizard split).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Previously the Caddy-location question lived inside run_site_configure,
gated behind 'Configure site defaults now? (y/n)'. Answering 'n' (e.g.
because Caddy is on a different box and you don't care about domain/tz
autofill) meant CADDY_MODE never got set, which silently disabled Caddy
prompts for every service for the life of the install (configure_caddy_for_service
falls through to mode 'none' and returns immediately).
Split into two steps:
1. ask_caddy_location() — always runs on first setup.sh invocation,
independent of any other prompt, and persists CADDY_MODE immediately.
2. run_site_configure() — now only asks timezone/domain/Caddy-network,
and is only offered when CADDY_MODE=local (those defaults are only
useful for FQDN autofill tied to a locally-managed Caddyfile).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Reorders the site defaults wizard so 'Where does Caddy run?' comes before
timezone/domain, since it's the more fundamental choice and the answer
context matters when explaining the other prompts. Also skips the Caddy
Docker network prompt entirely when Caddy isn't running locally — that
setting is only relevant to services joining a local Caddy container's
bridge network; remote/none mode proxies via localhost:PORT + snippet
files instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
The get.docker.com convenience script internally wraps every step in
'sudo -E sh -c ...'. On minimal/cloud Ubuntu images that never installed
the sudo package (common when operating purely as root), those internal
sudo calls silently fail while the outer script still exits 0 — apt never
actually runs, but no error surfaces. require_docker already runs as root,
so there's no need for sudo at all.
Replaced it with Docker's documented apt-repo steps run directly: add the
keyring, add the repo (with architecture/codename detected via dpkg and
os-release), apt-get install docker-ce + compose plugin, enable the
service. Real apt/curl/systemctl failures now propagate and print to the
terminal instead of being masked by the wrapper script.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
require_docker returning non-zero was silently ignored (no set -e).
Add explicit warning so the operator sees the failure; setup.sh already
has an unconditional Docker check after base that will retry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
The Docker check+install was inside the else branch that only runs when
base has never been installed. On re-runs (base already present) Docker
was silently skipped and only warned about. Move the check outside the
if/else so Docker is always installed if missing, regardless of whether
base was skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
whiptail fix:
- bootstrap.sh: redirect stdout and stderr to /dev/tty alongside stdin so
whiptail has full terminal control for raw mode (arrow keys, highlighting)
- setup.sh: run 'stty sane' on /dev/tty before the menu loop to reset any
stale terminal state from SSH reconnections or prior sessions
Installed-service summary:
- Print a grouped list of all currently-installed services before every
menu session so the operator knows the current state at a glance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Two fixes:
1. Export TERM (default xterm-256color) early — whiptail needs a valid
TERM to enter raw mode; when bash is started via pipe TERM may be
unset, causing keypresses to leak to the shell instead of the menu
2. Add </dev/tty to both whiptail calls so keyboard input always comes
from the controlling terminal regardless of how stdin was redirected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Three fixes:
1. configure_caddy_for_service: remove the '!= example.com' filter that
silently dropped any valid domain matching that string; now any non-empty
SITE_DOMAIN is used as the default subdomain suggestion
2. load_site_config: trim leading/trailing whitespace from key and val so
hand-edited .config files with extra spaces still parse correctly
3. setup.sh: call load_site_config after the site wizard saves so the
in-memory values are guaranteed fresh for all subsequent service installs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
- require_docker now runs as part of base so Docker is present on every box
- Install openssh-server, offer GitHub (gh:) and Launchpad (lp:) key import
via ssh-import-id; disable password auth only after keys are confirmed imported
- Handle Ubuntu cloud-init drop-in that re-enables PasswordAuthentication
- Offer NetBird install with optional --allow-server-ssh flag and setup key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
command -v may miss the binary if sudo stripped PATH; check the canonical
apt install location directly as a fallback before reporting failure, and
use the same fallback when printing the installed version.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Two bugs in require_docker:
1. apt post-install hooks (needrestart etc.) block on stdin which is
at EOF when running via pipe; DEBIAN_FRONTEND=noninteractive skips them
2. bash's command hash table doesn't pick up a newly installed binary;
hash -r flushes it so command -v docker finds /usr/bin/docker
Also moved usermod and success log after the binary check so [OK] only
prints when docker is actually reachable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
curl | bash consumes stdin from the pipe, so when bootstrap hands off
to setup.sh the script gets EOF immediately and exits with 'Cancelled'.
Redirecting </dev/tty restores keyboard input for the interactive menu.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
BASH_SOURCE[0] is unbound when bash reads from a pipe; set -u turns
this into a fatal error that no amount of :- or set +u reliably fixes
across bash versions. $0 is always set: 'bash' when piped (dirname
gives '.' where no setup.sh exists), and the correct path when run
directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
${BASH_SOURCE[0]:-} still triggers set -u when BASH_SOURCE is entirely
unset (not just empty) in pipe mode. Temporarily disable -u for that
single assignment, then restore it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
set -euo pipefail causes ${BASH_SOURCE[0]} to abort with 'unbound variable'
when the script is fed via curl | bash. Use ${BASH_SOURCE[0]:-} so the
variable expands to an empty string in that context, letting SCRIPT_DIR
resolve safely and the pipe path continue to the git-clone branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Three root-cause fixes found by reading the Docker image source:
1. Wine/Proton page fault: Docker's default seccomp profile blocks
system calls that Wine Proton GE requires. Fix: security_opt:
seccomp=unconfined + shm_size: 256m (Xvfb needs /dev/shm for
MIT-SHM extension; 64 MB default is too small).
Added network_mode: host for game traffic (dynamic UDP ports).
2. KYBER_MAP_ROTATION exit 64: the Kyber CLI decodes base64 and
parses newline-separated "MODE;MAP_PATH" lines, not JSON objects.
Our JSON [{map:...,mode:...}] format split on semicolons into one
field → ExitCode.usage (64). Fixed builder to emit MODE;MAP_PATH\n
lines and updated .env comment + README example.
3. GPU passthrough removed: Proton GE includes DXVK which crashes
headlessly when a GPU is passed through (no Vulkan display). The
server needs no GPU; removing passthrough is the correct fix.
Also: install libgamemode0:i386 on the host (Wine/Proton dep),
add alphanumeric-password warning (special chars → INVALID_PASSWORD).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014be1aK9G8CY2msho5LjxR4
dall-e-2/dall-e-3 retired May 12 2026 and gpt-image-1 deprecates Oct 23
2026, so move every OpenAI default (config.py, both provider classes,
both compose files, .env.example, the in-app provider-settings dropdown,
README) to gpt-image-2 for both generation and edits. Also fix response
parsing in ai_provider.py's OpenAIProvider, which never sent a model
param and assumed a url response — gpt-image-1/2 only return b64_json.
Separately, .env.example shipped AI_PROVIDER=replicate by default, but
replicate has no driver in remote_provider.py, so following the
documented "cp .env.example .env" setup silently broke every AI call
and defeated the GPU quick-start (an explicit non-empty .env value
overrides docker-compose.gpu.yml's own local_gpu fallback). Default to
local_gpu instead, mark replicate/stability as not-yet-implemented, and
recommend Lykon/dreamshaper-8-inpainting as a hands/face-tuned
HF_MODEL_INPAINT override for 4-6GB cards (Quadro P2200, GTX 1060/1660).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
inpaint()/img2img()/outpaint() called /v1/images/edits without a model
field, so OpenAI defaulted every cloud edit to dall-e-2 regardless of
configuration — while txt2img used dall-e-3. Add a separate
OPENAI_EDIT_MODEL (default gpt-image-1, the only current model that
supports masked edits at ChatGPT-comparable quality), thread it through
the provider and both compose files, and handle gpt-image-1's
b64_json-only response shape alongside the url shape dall-e-2/3 return.
setup.sh's run_service() looks up install_<name> using the literal
hyphenated registered name, not an underscore-converted one. borg-backup,
calibre-web, gaming-backup, and stirling-pdf all used underscored function
names and were therefore uninstallable ("has no install_<name>"). Same
fix already applied to ai-gpu/ai-stack; this closes out the rest of the
repo-wide audit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH