Blank already meant skip, but user feedback wanted a keystroke that says
so explicitly rather than just leaving the input empty. Added "0) Skip —
stay fully local" to the menu, updated the prompt to mention it, and
handled "0" as a silent no-op in the choice loop (previously it would
have fallen through to the "Ignoring unknown choice" warning).
The instruction was only in explanatory text a few lines above the actual
prompt (prompt_text "Cloud providers to add []:") — easy to miss once
that's scrolled past, especially since the bracketed default shows empty
but doesn't say what empty means. User feedback: the screen itself should
say it, not just text above it. Now reads "Cloud providers to add (blank =
skip, stay fully local):".
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.
Confirmed live: install_mealie() pre-computes BASE_URL as
recipes<suffix>.$SITE_DOMAIN before ever asking about Caddy, then
configure_caddy_for_service() separately prompts for a domain — which the
user can freely override (e.g. typing mealie.mydomain.com instead of
accepting the recipes.mydomain.com default). Nothing fed that choice back
into BASE_URL, so it stayed stale. Since BASE_URL is exactly what
_mealie_offer_authelia_oidc() registers as the OIDC redirect URI, this
produced Authelia's "redirect_uri does not match any of the OAuth 2.0
Client's pre-registered redirect_uris" — Caddy and DNS were both correctly
pointed at the new domain, but the client Authelia had on file still said
the old one.
Added CADDY_SERVICE_DOMAIN as a new configure_caddy_for_service() out-param
(lib/common.sh) — the same out-param convention as the existing
CADDY_SERVICE_CONFIGURED/CADDY_SERVICE_MODE, set right after the domain
prompt is accepted. install_mealie() now reconciles BASE_URL against it
immediately after the Caddy call, before the Authelia OIDC step reads
BASE_URL back out of .env. ActualBudget's equivalent OIDC offer asks for
its own domain fresh each time rather than reading a pre-computed BASE_URL,
so it isn't affected by this class of bug and needs no equivalent fix.
Verified the reconciliation logic in isolation against a synthetic .env.
Root cause of the recurring Mealie OIDC "unexpected character '/' in
variable name" failure, confirmed against the user's actual
configuration.yml byte content: this repo's own scripts write
authelia_url/domain unquoted, but YAML makes quoting optional, and a
hand-edited config can add single or double quotes around the value
(here: authelia_url: 'https://authelia.example.com.'). awk's
`print $2`/`print $3` is a naive whitespace-split token grab that doesn't
know about YAML quoting, so it captured the value WITH the literal quote
characters attached. The generated discovery URL then came out
`'https://authelia.example.com.'/.well-known/openid-configuration` —
Docker Compose's env parser closed the quoted value at that embedded
closing quote and choked on the trailing text as an invalid new token.
The earlier \r-stripping commit was a real but different fix (a
CRLF-tainted line fails to match these anchored awk patterns at all) —
it didn't cause and couldn't have fixed this. Both guards are needed and
now both apply, in both _authelia_provision_oidc_client() (domain and
portal URL) and the same latent bug in _authelia_add_oidc_client()'s
domain parse.
Verified end-to-end: reconstructed the user's exact reported byte
content (od -c dump) in a synthetic configuration.yml, ran the actual
_authelia_ensure_oidc_provider/_authelia_provision_oidc_client/
_mealie_offer_authelia_oidc functions against it (docker calls stubbed),
and confirmed the generated .env line is now a single clean line with no
embedded quotes or split.
The previous commit added OIDC_AUTHELIA_PORTAL_URL parsing but only
tr -d '\r'-sanitized the awk output, not the input. That's insufficient
for a CRLF-tainted file (confirmed live: a configuration.yml line
hand-edited by something that saves Windows line endings) — every
line-anchored awk pattern here fails to match at all against a line like
" cookies:\r", since $ anchors end-of-string and the \r is still part of
it, not just leaves a stray \r in the captured value. Symptom was Mealie's
generated OIDC_CONFIGURATION_URL line getting split mid-string, which
Docker Compose's env parser (bare \r treated as a line break too) reported
as "unexpected character '/' in variable name".
Fixed by piping the file through tr -d '\r' before awk sees it, for both
the domain and portal-URL parses. Verified against a synthetic CRLF config
that reproduces the exact failure — both fields now parse clean.
_authelia_provision_oidc_client() gained a new out-param,
OIDC_AUTHELIA_PORTAL_URL, read back from the instance's own
configuration.yml (session.cookies[].authelia_url) — the actual source of
truth for where the portal lives — instead of every caller separately
assuming "https://auth.$domain".
install_authelia() and add_authelia_domain() both still default new
instances to "auth." as before (unchanged), but that's just a default, not
a guarantee: it's plain text in configuration.yml and gets hand-edited on
some boxes (e.g. a dedicated instance renamed to "authelia." to avoid
colliding with another instance's "auth." on a different machine). Mealie,
ActualBudget, and Gitea's native-OIDC wiring all independently hardcoded
"auth." when building their discovery URL, so a renamed portal silently
produced a discovery URL pointing at a host that doesn't serve Authelia —
surfacing as an opaque 500 during the OIDC token exchange with no useful
client-side error.
Verified the new awk parse against both a default ("auth.") and a renamed
("authelia.") cookies block before trusting it.
edit_authelia_user() previously only ever let you select one user, act on
them, and then returned all the way out of install_authelia() (which calls
it with an immediate `return 0`) — deleting several users meant re-running
`sudo ./setup.sh authelia` and re-navigating to option 3 from scratch for
every single one.
Restructured: the per-user action menu (edit/reset-password/2FA/admin/
service-access/delete) is now _authelia_manage_one_user(), and
edit_authelia_user() drives it in a loop — numbered multi-select up front
("2 4" deletes/edits both), then "Manage more users?" to go again with a
freshly re-read user list instead of exiting. Guards against acting on a
user who was already deleted earlier in the same batch.
Verified against a synthetic users.yml: selecting two users by number and
deleting both in one pass removes exactly those two, leaves the others
untouched.
tr -cs 'a-z0-9_-' '-' only allowed lowercase letters, so any uppercase
leading character (e.g. "Bob") got converted to a dash and then stripped
by the paired leading-dash sed, silently truncating the username. Widened
to a-zA-Z0-9_- in both add_authelia_user() and _authelia_scope_access().
Also extends the "Manage an existing user" menu (still numbered-selection
throughout) with:
- option 6: toggle a user's membership in any existing "<service>-only"
scoped-access group, picked by number, via two new helpers
(_authelia_list_scoped_groups, and re-resolving the user's line range
before each toggle since a prior toggle in the same pass shifts it)
- option 7: delete a user outright (_authelia_delete_user_block), with confirmation
Verified against a synthetic users.yml (add/remove toggling across
multiple groups, block deletion, uppercase-username round-trip) before
touching the live file.
Confirmed live: re-running ActualBudget's Update path produced zero
output for the Authelia SSO step — no prompt, no message, straight back
to the shell. Root cause: the idempotency guards in
_actualbudget_offer_authelia_oidc / _mealie_offer_authelia_oidc /
_gitea_offer_actions_runner were plain `grep -q ... && return 0` — silent
by construction. Indistinguishable from the step not running at all,
which is exactly what it looked like.
ActualBudget and Mealie's OIDC offers now explain what they found and
ask whether to reconfigure (registers a fresh Authelia client + secret,
clearing the old env vars first) instead of silently bailing. Gitea's
Actions-runner offer explains what it found and how to check its status
(reconfiguring that one means editing a docker-compose service block,
not just a couple of env vars, so it just informs rather than offering
to redo it).
Also: _authelia_scope_access now shows existing Authelia usernames as a
numbered list before asking who should have access — picking by number
works alongside typing new names directly (mix freely, e.g. "1 3
newperson"), rather than requiring exact usernames typed from memory
with no reference and no protection against a typo silently creating a
duplicate account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
_authelia_scope_access asked for usernames to grant access to a service
without ever showing who already exists — confirmed live, the prompt
just showed a blank "Usernames:" line with nothing to reference. A typo
against an existing name doesn't fail or warn, it silently creates a new,
separate account instead of matching the intended one.
Now lists existing Authelia users (reusing _authelia_list_usernames,
already used elsewhere in this file) right before the prompt, and warns
about the typo/duplicate-account risk explicitly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Fixes two things found while answering a question about staying logged
in across every Authelia-protected service:
1. CLAUDE.md's own "stay logged in" instructions referenced
remember_me_duration — renamed to remember_me in Authelia 4.38, this
repo pins 4.39.20. Authelia doesn't error on an unknown key, it just
silently ignores it, so following that guidance as written would have
done nothing. install_authelia() itself already uses the correct
`remember_me` key at install time (default 7d) and was never affected
— only the hand-edit instructions in the docs were stale.
2. There was no way to change it afterward without hand-editing the file,
contrary to this repo's own "no manual config editing" direction.
Added _authelia_set_remember_me() (new menu option 7): prompts for a
new duration (12h/7d/1M/1y/-1 to disable), writes it, restarts.
Tested the sed replacement against a synthetic session block before
trusting it on real config. Also documented clearly (both in the
function's own prompt and in CLAUDE.md) that this only controls
Authelia's own session — a native-OIDC app's own session/token expires
on its own separate schedule, which this setting doesn't touch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Confirmed live: ActualBudget's new automated "Sign in with Authelia" offer
hit a client_id ("actualbudget") already registered from an earlier use of
the interactive "Register an app" menu — that older flow only registers
the client in Authelia and prints instructions to paste the secret into
the app's own settings manually; if that paste step never happened,
ActualBudget's .env never got the OIDC vars, but Authelia still considered
the client_id taken. _authelia_provision_oidc_client's duplicate check
just warned and returned 1, permanently blocking the automated offer with
no path forward — the stale registration's secret was shown once and
already gone, so there was nothing to recover, only reasons to replace it.
Added _authelia_remove_oidc_client() (tested against a synthetic
multi-client config, both mid-list and last-in-list removal) and changed
the duplicate-client_id check to remove-and-replace instead of failing.
Every automated caller (gitea/mealie/actualbudget's SSO offers) uses a
fixed, service-specific client_id, so a collision there means "this same
service was already registered," not a different app's ID being
clobbered. The interactive menu's own earlier duplicate check (a distinct
code path, one step before this one) is untouched — it still warns and
stops before prompting further, since a human-typed ID colliding with an
unrelated app is a different, more ambiguous situation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Researched which of the "has built-in auth" services actually support
native OIDC before wiring anything in (checked live docs, not assumed) —
two services turned out to contradict general assumption: Portainer's
OAuth/OIDC is Business Edition only (this repo installs portainer-ce, which
doesn't have it), and ntfy has no auth-oauth2-* support at all despite it
seeming like the kind of thing a modern self-hosted tool would have added
by now. Full findings recorded in CLAUDE.md so this doesn't need
re-researching.
Two real, verified wins wired up, both entirely env-var driven — no
manual config file editing, matching this repo's "no manual wizard"
philosophy and reusing the exact _authelia_provision_oidc_client /
_authelia_scope_access machinery already built for Gitea:
- mealie: OIDC_AUTH_ENABLED/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/
OIDC_CONFIGURATION_URL appended to the existing .env (env_file: .env is
already how mealie.sh's compose reads it). Also adds a
--forwarded-allow-ips entrypoint override when Caddy-fronted — confirmed
against Mealie's own issue tracker that without it, the generated OIDC
redirect URI comes out http:// even when actually served over https://,
which providers reject as a scheme mismatch.
- actualbudget: ACTUAL_OPENID_DISCOVERY_URL/CLIENT_ID/CLIENT_SECRET/
SERVER_HOSTNAME, same pattern. Redirect path (/openid/callback) matches
the preset already used by authelia.sh's own "Register an app" menu for
this same service.
Both offered on fresh installs and Update reruns, default no, and both
call _authelia_scope_access() afterward so access can be restricted to
specific users instead of every Authelia user, same as Gitea.
Immich has real OIDC + a system-config API but needs one more
verification pass on the exact request payload before automating — not
guessing that part. Jellyfin and Home Assistant only have third-party
plugin/HACS-based OIDC, a bigger lift than an env-var toggle — noted but
not attempted this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Every domain with an access_control rule was reachable by any Authelia
user by default (the existing catch-all *.${AUTHELIA_DOMAIN} rule) — no
way to restrict a specific service to a subset of users without hand-
editing configuration.yml and users.yml directly.
_authelia_scope_access(SERVICE_ID, DOMAIN) is a new generic, reusable
helper: call it after any service finishes being protected by Authelia
(forward_auth gate or native OIDC alike — it only cares about the domain).
Offers universal vs. specific-users access; if scoped, creates a
"<service_id>-only" group, adds every listed username to it (creating
accounts on the fly for names that don't exist yet, via the new
_authelia_create_user_noninteractive — a non-interactive sibling to
add_authelia_user, same extraction pattern already used for
_authelia_provision_oidc_client), and inserts an allow+deny rule pair
above the general catch-all. Idempotent on rerun.
_authelia_report_access_scope() (new menu option 6) is the read side —
lists who has universal vs. service-scoped access, and offers to promote
a scoped user back to universal by removing their "-only" group
membership.
services/gitea.sh's _gitea_offer_authelia_sso() is the reference
integration, calling _authelia_scope_access after successfully wiring up
Gitea's OIDC login. The other services with a plain "Protect X with
Authelia?" prompt (magicmirror, wolf-pair, js99er, drum-rhythm-game,
iopaint, paintplus, stirling-pdf, wolf) are natural follow-ups once this
is confirmed working live — each just needs one added call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Sipnetic clients on the user's home WiFi/VLAN lose SIP/TLS registration
every ~5s when Asterisk runs on IONOS, but never on DigitalOcean or over
mobile data — CrowdSec, OPNsense firewall/IDS, TURN-for-registration, and
raw packet loss have all been ruled out via live testing. Leading theory
is an idle-connection timeout inside IONOS's network virtualization layer.
keep_alive_interval sends a periodic double-CRLF over the TLS transport to
keep it from going idle, the standard mitigation for this failure class.
Follows the existing dual-patch pattern (vendor-template copy + live file)
since transport objects aren't picked up by `pjsip reload` and need a
container restart to apply, same as the live_dangerously fix.
Gitea Actions is Gitea's own CI, largely GitHub-Actions-workflow-compatible
(.gitea/workflows/*.yml). Off by default; this Gitea install is otherwise
just a passive GitHub pull mirror, so the main value here is resilience —
.gitea/workflows/*.yml can still run something like a GitHub Actions build
if GitHub itself is ever unreachable.
_gitea_offer_actions_runner(), offered on fresh installs and Update reruns
(idempotent — no-ops if already set up):
- Enables GITEA__actions__ENABLED / DEFAULT_ACTIONS_URL in the compose
file's environment, restarts to apply
- Generates a runner registration token via `gitea actions
generate-runner-token`
- Appends an act_runner service to the same docker-compose.yml, using
the host's Docker socket to launch a fresh container per job — the
same pattern this repo already uses for portainer/watchtower/
uptimekuma/beszel/traccar's autoheal
- Falls back to printing manual setup instructions if token generation
fails, rather than losing the attempt silently
_gitea_fix_ownership()'s data/-exclusion (added when we fixed the earlier
SQLite readonly-database bug) now also skips runner-data/, so a future
reinstall/update doesn't clobber the runner's own state the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Gitea has its own built-in login, so it was never wired into the
forward_auth/Caddy pattern the rest of this repo uses to gate apps with
no auth of their own — that's still correct and unchanged. But Gitea
also supports adding an OAuth2/OpenID Connect authentication source
natively, and Authelia can act as an OIDC provider — a genuinely
different, additive integration: an extra "Sign in with Authelia" button
on Gitea's own login page, alongside local login, not a Caddy-level gate.
Refactored services/authelia.sh's _authelia_add_oidc_client() to split
out its non-interactive core as _authelia_provision_oidc_client() — same
behavior for the existing ActualBudget/Vaultwarden/Immich/custom-app menu
flow, but now callable directly by other services with explicit args
instead of walking a human through the menu, returning the plaintext
secret and Authelia's domain via out-params.
services/gitea.sh's new _gitea_offer_authelia_sso() uses that to fully
automate both sides when accepted: registers Gitea as an OIDC client in
Authelia, then runs `gitea admin auth add-oauth` itself to add Authelia
as an authentication source — no manual web-UI copy-paste on either side,
matching how this installer already avoids manual wizards for the admin
account/token. Falls back to printing the values for a manual add if the
Gitea-side CLI call fails. Offered on fresh installs and on Update
reruns (default no, so a plain Update stays silent), so it can be added
later without a full reinstall.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Root cause of "attempt to write a readonly database (1544)" on repo
creation: Gitea's container always runs internally as UID 1000
(USER_UID/USER_GID are fixed in docker-compose.yml, independent of
whoever's running this installer) — the image chowns /data to that UID
itself at startup. install_gitea()'s three ensure_docker_dir_ownership
calls recursively chown the *entire* service directory, data/ included,
to $ACTUAL_USER. On a box where the installer runs as root directly
(ACTUAL_USER=root), that resets a live data/ back to UID 0. If the
container doesn't happen to restart right after — confirmed live: Update
mode against an already-running container just no-ops instead of
restarting — nothing ever re-fixes it, and every subsequent write to
Gitea's own SQLite DB fails.
Added _gitea_fix_ownership(), which chowns everything in the service
directory except data/, and swapped it in at all three call sites. The
container continues to own data/'s permissions exclusively, as it always
has on first boot; this installer no longer fights it on every rerun.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Root cause of the "Failed to reach Gitea API" / 403 errors on every retry:
`gitea admin user change-password` (used in the already-exists branch to
sync the account's password to what the user just entered) defaults to
setting must_change_password=true, unlike `user create` which was already
pinned to --must-change-password=false. Once set, Gitea rejects every API
call — including the sync script's own token-authenticated calls — with
403 "You must change your password", even though the token itself and
GITEA_URL were both completely correct. Confirmed live via a direct curl
against /api/v1/user.
Pin the same flag on change-password that create already used.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Live symptom: pasting the GitHub PAT over SSH showed the token text
landing on the terminal *after* "No GitHub token entered" had already
printed — the prompt's read() returned empty a beat before the paste
actually arrived (a paste/Enter race that isn't specific to this box,
just common over higher-latency SSH sessions). A single empty answer
was treated as "user has no token" and the install moved on silently.
Both token prompts (GitHub token, and the Gitea-token manual fallback)
now retry up to 3 times interactively before giving up, and strip
whitespace from what was captured in case the paste carried a stray
leading/trailing newline. Unattended installs still take one shot, same
as before, since nobody's there to retry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
The sync direction step only ever set up the timer (or printed manual
instructions) — there was no way to actually confirm tokens/config work
without waiting for the first scheduled run or invoking the script by
hand afterward. Add a post-configure prompt: dry-run preview (--list),
run for real right now, or skip. Defaults to dry-run interactively;
defaults to skip under UNATTENDED so a headless install with no GitHub
token configured doesn't spam preflight errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
generate-access-token used a fixed --token-name "sync", which Gitea
rejects on a second call for the same user (e.g. a retry against an
already-existing admin account, now common after the readiness-wait
fix). The failure was silent: it fell through to a manually-labeled
"Paste the Gitea token here" prompt appearing immediately before the
real "GitHub token:" prompt, so a pasted GitHub PAT could land on the
wrong prompt and leave GITHUB_TOKEN empty with no clear reason why.
Token name now includes a timestamp so it's always unique, and the
fallback prompt is relabeled to make clear it wants a Gitea token, not
the GitHub one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Previously the admin account was silently auto-generated (username =
ACTUAL_USER, random password) and only created after a fixed 60s
readiness probe — a slow first boot (SQLite init on a slower disk) timed
the whole install out with no account ever created, leaving the user to
create one by hand with a raw docker exec.
Now the install prompts for admin username/password up front, then folds
account creation into the same retry loop used to detect readiness (up to
2 minutes), so a slow-but-eventually-successful boot no longer dead-ends
the install. A retry against a partially-completed prior run (account
already exists) is treated as success and syncs the password instead of
failing outright.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
CONTAINER_NAME was correctly re-detected from the current layout
(ASTERISK_KIND) at the top of install_pstn-trunk(), then immediately
clobbered by sourcing the saved .pstn-trunk.env settings file — and
_pstn_apply_settings writes CONTAINER_NAME straight back into that
same file every run, so a stale value self-perpetuates forever once
it's wrong.
Confirmed live: a box migrated from a DigitalOcean droplet (where the
settings file correctly saved CONTAINER_NAME=easy-asterisk-do) to a
plain/home install kept that droplet-era value indefinitely, since
only "update" runs happened after the migration. Every update
reloaded/restarted a container that no longer existed instead of the
one actually running Asterisk, so dialplan changes (including
pstn-trunk-inbound-dialplan.conf) never reached the live process even
though the config files themselves were written correctly.
Re-asserts the freshly-detected value after the source instead of
trusting whatever was saved, so it can't drift from the box's actual
current layout and self-heals the persisted file on the next update.
Gitea previously only existed bundled inside the full ai-stack service
(Ollama/ComfyUI/InvokeAI/etc. all together) — no way to get just a git
server without the rest of that heavy stack. This adds it as its own
lightweight service, reusing the vendored gitea-github-sync.sh but not
any of ai-stack's other components.
- Auto-creates a Gitea admin account and API token via the container's
own CLI (no manual web setup wizard).
- Asks GitHub token, sync direction (GitHub->Gitea / Gitea->GitHub /
both), and whether to install a systemd timer for automatic sync —
prints manual commands instead if declined.
- Own systemd unit for the timer rather than the vendor script's
built-in --install-timer, since that always runs both directions
with no way to pin a single direction.
pstn_personal_ring dialed the owner and hung up regardless of
DIALSTATUS, so no-answer/busy calls to a personal DID just dropped
silently instead of offering voicemail. Now falls to
VoiceMail(<owner>@default,u) on anything but ANSWER, gated by the
owner's existing voicemail=yes/no flag in pstn-permissions.conf.
The shared ring-group and group-owned personal DID inbound paths have
the same gap but no single owning extension to pick a mailbox for —
left as-is pending a decision on what that should do.
Adds live_dangerously = yes to asterisk.conf automatically on install/
update, fixing silent PSTN call denial (AST_CONFIG() returning empty
with no error when this option is off).
AST_CONFIG() silently returns an empty string instead of erroring when
asterisk.conf's [options] section lacks live_dangerously = yes, so a
correct tier_out=full in pstn-permissions.conf still evaluates as no
permission — every outbound/ring-group call gets denied with nothing
in the logs pointing at the real cause. Easy Asterisk's vendor default
ships without this set. Now applied automatically on fresh install and
on every "update" rebuild, restarting only when the file actually
changes.
Immich has full native OIDC support (its own docs list Authelia as a
supported provider), but needed more than the single-redirect-URI
model _authelia_add_oidc_client() previously supported: it requires
three redirect_uris at once (web login, account-linking page, and the
mobile app's app.immich:///oauth-callback custom-scheme redirect).
Generalized redirect-URI handling from a scalar REDIRECT_PATH to two
arrays (domain-relative REDIRECT_PATHS, plus already-complete
EXTRA_REDIRECT_URIS for non-domain-based ones like the mobile scheme)
and build the YAML redirect_uris list from however many are present.
ActualBudget/Vaultwarden/Other still resolve to a single-entry array,
so their generated config is unchanged. Verified the multi-entry YAML
generation against a python yaml parser before wiring it in, and the
case-statement/array logic in isolation against the real file's code.
Confirmed live (not from docs): Authelia's in-portal Settings -> Change
Password also emails a one-time code to confirm, same as Forgot
Password — it is not a no-SMTP path as earlier text here assumed.
Reworded all three spots in authelia.sh that claimed otherwise to
point at the admin-side "Edit an existing user" -> "Reset password"
action instead, which never touches email.
Also corrected CLAUDE.md's "No built-in auth — should be protected"
list per an actual grep of services/*.sh: it was missing
drum-rhythm-game, iopaint, paintplus, stirling-pdf, wolf, and the
unconditionally-protected security-dashboard/asterisk, and wrongly
included sky-cam (a non-Docker batch script with no web UI or Caddy
integration at all, nothing for Authelia to protect).
New menu option lists existing users by number; picking one opens a
submenu to edit email/display name, force a password reset, reset a
2FA device (authelia storage user totp delete), toggle a one_factor
exemption for that user via a subject-scoped access_control rule, and
promote/demote admin group membership.
All the YAML-editing helpers (line-range lookup, scoped field/group
edits, and the access_control rule insertion/removal used by the 2FA
exemption toggle) are line-range-scoped to the target user only, and
were verified against single- and multi-domain/multi-user fixtures
before wiring them into the interactive flow — a bad edit to
access_control here would break every protected domain, not just one
user's account.
30 characters, guaranteed at least 5 uppercase, 5 digits, and 5 special
characters, shuffled. Scoped to add_authelia_user() only via a small
local generator — deliberately not routed through lib/common.sh's
shared generate_password, since that one is alphanumeric-only by
design (its paired validate_password rejects special characters) and
plenty of other services embed its output unescaped into .env/YAML/URLs.
ActualBudget requires inviting additional OpenID users from its own
"Server Online" screen before their login is accepted, separate from
Authelia authenticating them successfully. Companion doc gets appended
to the generated README automatically (write_readme convention).
Adding a user previously required hand-editing users.yml and generating
the argon2 hash manually. New menu option (2) on an existing Authelia
install prompts for username/email/display name/admin group, generates
the hash and temp password, inserts the users.yml block, and restarts
Authelia — mirroring the existing add_authelia_domain/OIDC-client flows.