Commit Graph
541 Commits
Author SHA1 Message Date
Claude cfee4b292b docs(ai-stack): add local-GPU + Claude Code hybrid workflow section
Covers the CLAUDE.md tiering (user/project/local/one-off task) for
splitting instructions between the local coder model and Claude Code,
plus how Claude Code reads from self-hosted Gitea (plain git vs. the
official gitea/gitea-mcp server for PR/issue-level integration).
2026-08-22 04:22:51 +00:00
Claude c703b1c4b4 docs(ai-stack): disambiguate Tesla brand name from Tesla microarchitecture
"Tesla" was NVIDIA's datacenter-card brand (retired after Volta), spanning
four unrelated architecture generations (Kepler/Maxwell/Pascal/Volta) - a
different, much older thing than the Tesla microarchitecture itself, which
actually predates Fermi/Kepler/Maxwell/Pascal/Volta. Clarify to avoid
reading the power-connector note as claiming Tesla == Volta.
2026-08-22 03:55:27 +00:00
Claude 7f23860078 docs(ai-stack): name the R580 driver branch for Volta/Pascal cards
Confirmed via NVIDIA's own release notes: R580 is the last branch
adding Volta/Pascal support (through ~June 2028). Also flag the
proprietary-vs-open kernel module split, since -open only covers
Turing and newer and autoinstall could pick it on a newer distro.
2026-08-22 03:52:39 +00:00
Claude 503945e7c3 docs(ai-stack): correct CUDA deprecation status, add Tesla power/cooling note
CUDA 13 already dropped Pascal/Volta (not a future warning anymore) -
call out pinning the driver branch instead. Also flag the 8-pin
CPU/EPS12V power connector and passive cooling on Tesla PCIe cards,
since it trips up anyone repurposing one in a tower case.
2026-08-22 03:46:06 +00:00
Claude f5a23b06ac docs(ai-stack): note MoE models as a coding option for Pascal/Volta
MoE VRAM tracks total params but compute tracks active params, so a
model like qwen3-coder:30b-a3b (30B total, ~3.3B active) runs light
enough that old GPUs' weak/missing tensor cores barely matter.
2026-08-22 03:41:25 +00:00
Claude e54c7827de docs(ai-stack): add server GPU generation capability table
Reference table for Blackwell/Hopper/Ampere/Volta/Pascal/Maxwell covering
Flux 2, Flux.1/SDXL, chat, and coding model capability per generation.
2026-08-22 03:22:02 +00:00
Claude 4eda82acce asterisk.sh: don't flag chained services as unwired when Caddy is remote
The stack health check's Security Dashboard/sms-inbound/ntfy checks only
had visibility into this box's own local Caddyfile — but all three can
legitimately be fronted by a Caddy (and Authelia) on a completely
different box instead, the same remote-Caddy pattern sms-inbound.sh and
ntfy.sh's own installers already support via CADDY_MODE/CADDY_REMOTE_HOST.
A site explicitly configured for remote Caddy was getting a false "Caddy
has no site block for it" for each of them, with a fix offer that would
have been actively wrong: adding a redundant local Caddy block for
something deliberately fronted elsewhere.

Now resolves the same site-wide CADDY_MODE the affected services'
installers themselves use before treating "not found locally" as a real
issue — only counts it, and only offers a fix, when the site is actually
in local Caddy mode. Remote (or no-Caddy) mode gets a plain informational
line instead: not wrong, just not something this box can verify.

Verified: default/local mode still flags a genuinely unwired dashboard as
an issue with a fix prompt; CADDY_MODE=remote (even with a local Caddy
directory also present) correctly downgrades the same finding to
informational with no prompt and no issue counted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 22:54:02 +00:00
Claude d1a3c4b0be asterisk.sh: proactive stack health check in update mode
"update" mode's whole promise is leaving already-configured things
alone — but that assumption silently breaks when something was
configured but never fully wired up, and update never re-asks the
questions that would reveal it. This session hit three separate
instances of exactly that on one droplet revert: a domain set with no
Caddy block, a Caddy block with no synced TLS cert (transport-tls fails
to bind — "Unable to retrieve PJSIP transport 'transport-tls'", breaking
every call), and a baked-in external IP left over from before the box
moved. sms-inbound and (potentially) Security Dashboard/ntfy can have
the identical "domain set, nothing serving it" gap with no way to
discover it either, since their own update modes don't re-ask.

_asterisk_run_stack_health_check(), called every "update", replaces the
narrower Caddy-only check added last time:
- Compares pjsip.conf's baked external_signaling_address against this
  box's actual current public IP; offers to rewrite it and restart.
- Checks Asterisk's own DOMAIN_NAME has both a Caddy site block and a
  matching TLS cert in the container; offers to fix each independently.
- Checks Security Dashboard / sms-inbound / ntfy (whichever are
  installed) for a matching Caddy site block, via a new lib/common.sh
  helper (caddy_domain_for_upstream) that finds the block without
  needing to already know the domain — none of these three services
  persist it anywhere. Points at that service's own "Full reinstall"
  (the only mode that re-asks) since fixing their config isn't this
  file's to script.

The cert-sync fix needed a non-interactive hook into the vendored
easy-asterisk CLI, which only exposed it as an interactive menu item
(Server Settings -> Force re-sync Caddy certs). Added a --sync-caddy-cert
flag via _asterisk_patch_cert_sync_cli(), patching the deployed vendor
copy the same way _asterisk_patch_voicemail_vendor_files and friends
already do — never vendor/ in git.

Every check runs unconditionally (never opt-in, so a gap is never missed
by nobody thinking to ask); every fix is individually opt-in and named
as a real config change, unlike the rest of "update"'s no-side-effects
default.

Also factored the DO-metadata/ifconfig.me/hostname-I public-IP detection
chain (previously duplicated 3 times) into _asterisk_current_public_ip().

Verified: caddy_domain_for_upstream against a multi-block Caddyfile
(distinguishes same-prefix upstreams correctly); the full health check
against fake docker/curl across every combination (all wired, IP
mismatch declined/accepted, cert mismatch declined/accepted, dashboard
unwired, sms-inbound wired vs. placeholder-domain, multi-instance ntfy
with one wired and one not); and _asterisk_patch_cert_sync_cli's
idempotency + resulting syntax against a real copy of the vendor script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 22:45:36 +00:00
Claude b7691e6c1b asterisk.sh: offer to fix Caddy when DOMAIN_NAME is set but unwired, in update mode
"update" mode never re-asks the domain/networking/Caddy questions, on the
assumption there's already Caddy/Authelia config in place to leave alone.
That assumption breaks for an install where a domain was set at some
point (DOMAIN_NAME in .env) but Caddy never actually got a site block for
it — declined at install time, DNS wasn't ready yet, or Caddy was
reinstalled/reset separately since. Previously the only way back was a
full reinstall, which re-generates a dedicated coturn container with new
TURN credentials (every already-configured phone needs its QR re-scanned)
— a lot of blast radius just to add one missing Caddy block, and enough
that reaching for it risks the extensions/voicemail data a "fresh"
reinstall can also wipe if the wrong prompt is answered.

"update" mode now detects this specific gap (domain set, no matching
Caddyfile block) and offers to run _asterisk_configure_caddy_public()
right there — the same function "fresh" installs use, but it only ever
touches the Caddyfile and .env's WEB_ADMIN_AUTH_DISABLED line, never
coturn/extensions/anything else "update" already promises not to touch.

Verified in isolation: offers and calls the fix when the domain is set
with no matching Caddyfile block, stays silent when a block already
exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:55:15 +00:00
Claude da59b65ceb Wire up MWI so voicemail notifications actually reach the phone
Confirmed live: neither this repo nor the vendored easy-asterisk script
ever sets a PJSIP endpoint's `mailboxes=` field. add_device()'s own
device_config template never writes it, and write_voicemail() only ever
touched voicemail.conf — so recording a voicemail worked fine
(voicemail.conf + the dialplan's VoiceMail() call), but no phone ever
actually subscribed to be told about it, regardless of whether the
voicemail flag was on. Matches the exact symptom of "voicemail records
fine, but no notice comes up on the phone."

Add _ea_set_endpoint_mailboxes(), called from write_voicemail(): adds/
updates mailboxes=<ext>@default in that extension's PJSIP endpoint stanza
when voicemail is enabled, removes it when disabled, and reloads
res_pjsip so it takes effect immediately. Bounded to just the
type=endpoint stanza (pjsip.conf reuses the same [ext] bracket name for
type=endpoint/type=auth/type=aor) the same way lib/common.sh's
_remove_caddy_site_block is bounded for Caddy blocks — verified against a
two-device pjsip.conf that editing one extension's mailboxes= never
touches its own auth/aor stanzas or another extension's stanzas, that a
repeat enable doesn't duplicate the line, and that disabling removes it
cleanly.

Existing extensions with voicemail already enabled won't get this
retroactively — the Extensions tab's voicemail toggle has to actually run
again (off then back on) to apply it, since this only fires on the
enabled/disabled transition itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:37:52 +00:00
Claude b0d51f0344 Fix sms-inbound.sh persisting an unusable "<your-domain>" webhook URL
When the "Public domain for the webhook" prompt was left blank (DNS not
ready yet, or just missed), the installer built the Forward-to-URL as
literal https://<your-domain>/sms/... and persisted that placeholder to
settings.env as if it were real. "Update" mode never re-prompts for the
domain (by design — it's meant to leave already-configured settings
alone), so every later re-run silently re-served the same unusable
placeholder, with nothing indicating anything was wrong. A DID provider
(Anveo) correctly rejects it — it isn't a resolvable hostname.

- Only build FORWARD_URL when a real domain was entered; leave it empty
  otherwise instead of substituting the placeholder.
- Fresh-install summary and README now say plainly that setup isn't
  complete and how to finish it, instead of printing an empty/bogus URL.
- Update-mode now detects a missing/placeholder domain and tells you to
  re-run with "Full reinstall" to be asked again, instead of reporting
  success with a broken URL.

Verified with a direct test of _sms_write_readme() and the FORWARD_URL
construction for both the blank- and real-domain cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:13:16 +00:00
Claude be60f475c3 Add zero-click Authelia login to Gitea and Uptime Kuma
Follow-up to Frigate's Authelia integration: both of these can also skip
their own login entirely once Authelia is doing the gating, each with a
different trust model appropriate to what the app actually supports.

- gitea: new _gitea_offer_reverse_proxy_auth(), a second Authelia
  integration alongside the existing OIDC "Sign in with Authelia" button.
  Enables Gitea's own ENABLE_REVERSE_PROXY_AUTHENTICATION so it auto-logs
  in from a trusted Remote-User header — no click, no separate Gitea
  session to expire on its own. Trust is IP-range based
  (REVERSE_PROXY_TRUSTED_PROXIES), computed from caddy_net's real subnet
  the same way ufw_allow_from_caddy_net does; refuses to enable the
  feature at all if that can't be determined rather than fall back to a
  permissive default — Gitea's own Docker image has shipped an unscoped
  default before (GHSA-f75j-4cw6-rmx4, any IP could impersonate any user).
  Rewires Gitea onto caddy_net and re-points Caddy at gitea:3000, since it
  previously only reached Caddy via its published host port. Gitea's own
  login stays available as a fallback, so unlike Frigate there's no
  "native login off with nothing gating it" state to guard against.

- uptimekuma: sets DISABLE_AUTH=true only once Caddy's "import authelia"
  gate is confirmed in front of it. Uptime Kuma already joined caddy_net
  unconditionally, so this only needed the env var plus moving the
  Authelia-gated Caddy call earlier (before docker-compose.yml is
  written); the existing unconditional call at the end now only runs as a
  fallback when the Authelia path wasn't used or wasn't completed. Kuma's
  DISABLE_AUTH has no IP-scoping or secret check left once set — the
  strictest of the three to get the ordering right on, since a mistake
  here means wide open, not just spoofable.

Verified with a local test harness (fake Authelia/Caddy/docker-network
state): both the happy path and the "Caddy declined" safety fallback
produce the expected docker-compose.yml/.env/Caddyfile output for each
service, and Gitea's subnet-detection refusal + idempotent-rerun guard
were exercised directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:07:02 +00:00
Claude cd33b7ce71 Add Authelia SSO to Frigate — disables its own login, not just a gate in front of it
Frigate has its own built-in login separate from Authelia's session, so
just adding `import authelia` in front of it (the pattern used for
no-built-in-auth services) would leave two independent logins stacked,
defeating the point of Authelia's "remember me" on mobile. Frigate has a
`proxy` auth mode built for exactly this — trust Remote-User/Remote-Groups
from an upstream forward_auth proxy and disable its own login entirely.

- Extend configure_caddy_for_service() with an optional 5th arg for
  sub-directives inside the reverse_proxy block itself (header_up), needed
  to pin an X-Proxy-Secret header so Frigate's proxy-auth trust can't be
  spoofed by a request reaching its published port directly, bypassing
  Caddy/Authelia. Backward compatible — every other caller is unaffected.
- services/frigate.sh: prompt to protect with Authelia when installed;
  wires import authelia + the X-Proxy-Secret header_up into Caddy, and
  only writes config.yml's auth.enabled: False + proxy block once Caddy
  actually confirms it's fronting the domain (never disables the native
  login with nothing else gating access). Reuses the secret across
  reinstalls instead of rotating it. Calls _authelia_scope_access() so
  access can be restricted to specific users instead of every Authelia
  account. Fixed a latent bug in the standalone-mode Caddy stub where the
  auth block was placed after reverse_proxy instead of before it (dead
  code — the same "Authelia never prompts" bug class CLAUDE.md documents
  for the real helper).
- CLAUDE.md: document the new configure_caddy_for_service parameter and
  Frigate's hybrid built-in-auth/forward_auth pattern.

Verified end-to-end against a local test harness (fake Authelia/Caddy
dirs): config.yml, .env, and the generated Caddyfile block all agree on
the shared secret and header names, auth is skipped cleanly when Caddy
isn't configured, and the secret is reused on a second run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 19:56:27 +00:00
Outis afc59613fc Merge pull request #374 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
2026-08-21 11:29:24 -04:00
Claude 3b4c238e1d ai-stack: add RAG/MCP stack to the optional-services picker, with dependency cascade
User feedback: wanted chromaDB/rag-server/mcp-server included in the
optional-services picker added last commit, not just Gitea/Portainer/
Kiwix/InvokeAI/ComfyUI/Aider. Confirmed against the compose file's
depends_on chain before adding: open-webui only depends_on ollama (its
OLLAMA_BASE_URL connection works standalone), so none of these three are
actually required for regular chat — only Open WebUI's separate RAG tab
(routed through rag-server) and MCP tool-calling need them. Mealie's own
Ollama usage never touches this stack at all.

Bundled chromadb+rag-server+mcp-server as one option (7), not three
separate numbers — mcp-server depends_on rag-server depends_on chromadb,
so stopping only one of the three would leave the others running against
a dead dependency instead of a clean stop. Also added a cascade for the
existing kiwix option: mcp-server depends_on kiwix too (not just
rag-server), so stopping kiwix without also stopping mcp-server has the
same problem — now handled automatically with a dedup pass in case both
the kiwix cascade and option 7 add mcp-server to the stop list.

Verified all four cases in isolation: kiwix-only correctly cascades to
mcp-server, option 7 alone stops the right three, choosing both dedupes
to one clean list, and unrelated choices (gitea/portainer) are unaffected.
2026-08-21 15:23:18 +00:00
Claude 596bd4a9d5 ai-stack: offer to stop optional services after install instead of always running everything
User feedback: local-ai-setup.sh always brings up the entire stack
unconditionally (Gitea, Portainer, Kiwix, InvokeAI, ComfyUI, Aider,
alongside the core Ollama/Open WebUI/ChromaDB/RAG/MCP) with no way to opt
out — e.g. Gitea when you already run git elsewhere, or Portainer when you
manage Docker some other way.

Didn't touch local-ai-setup.sh's own compose generation for this (it's
vendored upstream code, and other services reference these by container
name/network in ways that would need individual auditing to make safely
conditional). Instead, added a post-install picker in the wrapper: after
the full stack starts, offer to `docker compose stop` whichever of the six
non-core services aren't wanted. Images are already pulled either way, so
anything stopped comes back later with a plain `docker compose up -d
<name>` — no reinstall needed.

Verified the choice-parsing loop in isolation: "1 4 9 3" correctly warns
on the invalid "9" and resolves to gitea/invokeai/kiwix.
2026-08-21 15:11:33 +00:00
Outis 88d48e0e74 Merge pull request #373 from outis1one/claude/gitea-standalone-setup-oxoi2e
vaultwarden: require/enforce an http(s):// scheme on DOMAIN
2026-08-21 10:00:37 -04:00
Claude 1018dd8c9e vaultwarden: require/enforce an http(s):// scheme on DOMAIN
Confirmed live: a bare hostname in DOMAIN= (typing "vault.example.com"
instead of "https://vault.example.com" at the install prompt — easy to do
despite the example text showing the scheme) crash-loops the container
with no clear startup error, and re-running the installer doesn't fix an
already-written .env since "update" mode deliberately never touches it.

Two changes, mirroring how the existing SMTP half-state bug is already
handled in this file:
- Normalize VW_DOMAIN at prompt time — missing scheme gets https://
  prefixed automatically instead of writing it verbatim.
- New _vaultwarden_fix_domain_scheme() self-heal, called at the same two
  sites as _vaultwarden_fix_smtp_halfstate() (the "update" path and the
  fresh-install "start now" path), so a box that already has a scheme-less
  DOMAIN self-heals on its next start instead of staying stuck.

Verified the self-heal function in isolation: vault.mydomain.com ->
https://vault.mydomain.com.
2026-08-21 05:19:25 +00:00
Outis ed9251580a Merge pull request #372 from outis1one/claude/gitea-standalone-setup-oxoi2e
ai-stack: chown the stack dir back to ACTUAL_USER after every install…
2026-08-21 00:09:23 -04:00
Claude 1ff12d6643 ai-stack: chown the stack dir back to ACTUAL_USER after every installer run
local-ai-setup.sh runs as whoever invoked this wrapper — root, since
setup.sh itself runs under sudo — so every file it generates
(docker-compose.yml, .env, requirements.txt, server.py, mcp_server.py,
pull-models.sh, start/stop/status.sh) came out root-owned. Nothing handed
that back to ACTUAL_USER unconditionally: the only existing
ensure_docker_dir_ownership call was inside the cloud-provider wiring
block, so it silently never ran at all for anyone who skipped cloud
providers.

Confirmed live: this repo's own "Skipped. Run later: cd $AS_DIR && bash
local-ai-setup.sh" message tells the user to re-run it directly later as
themselves (no sudo) — which then fails with "Permission denied" on any
file root created during the original sudo run, e.g. requirements.txt.
Same root cause class as a stray root-owned .git/FETCH_HEAD blocking a
plain `git pull` — a root-run leaving files a later unprivileged run can't
touch.

Fix: call ensure_docker_dir_ownership "$AS_DIR" unconditionally right
after the installer-run block, not only on the cloud-provider path.
2026-08-21 04:05:27 +00:00
Outis 45f5c6c1e1 Merge pull request #370 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
2026-08-20 23:52:25 -04:00
Claude b4eccbd02d ai-stack: accept 0 as an explicit "skip cloud providers" choice
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).
2026-08-21 03:51:41 +00:00
Claude 4399e8db71 ai-stack: state "skip = stay fully local" on the prompt line itself
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):".
2026-08-21 03:50:12 +00:00
Outis 22d213025d Merge pull request #369 from outis1one/claude/gitea-standalone-setup-oxoi2e
ai-stack: offer an optional vision-capable Ollama model, including mo…
2026-08-20 23:27:17 -04: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
Outis 0a798d5ec9 Merge pull request #368 from outis1one/claude/gitea-standalone-setup-oxoi2e
mealie: reconcile BASE_URL with the domain actually chosen at the Cad…
2026-08-20 21:19:41 -04:00
Claude ebe8ea3245 mealie: reconcile BASE_URL with the domain actually chosen at the Caddy prompt
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.
2026-08-20 22:03:54 +00:00
Outis 7a8b8b001b Merge pull request #367 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: strip YAML quoting from parsed domain/portal-URL values
2026-08-20 17:11:08 -04:00
Claude 7aef571b27 authelia: strip YAML quoting from parsed domain/portal-URL values
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.
2026-08-20 21:10:22 +00:00
Outis 9ec4ee7963 Merge pull request #366 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: strip CR before parsing configuration.yml, not after
2026-08-20 16:26:20 -04:00
Claude 86331b541d authelia: strip CR before parsing configuration.yml, not after
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.
2026-08-20 20:22:42 +00:00
Outis 96741c4f32 Merge pull request #365 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: stop hardcoding "auth." as the portal subdomain for OIDC di…
2026-08-20 16:18:50 -04:00
Claude fc3b85f970 authelia: stop hardcoding "auth." as the portal subdomain for OIDC discovery
_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.
2026-08-20 20:17:58 +00:00
Outis 9ecf37c56f Merge pull request #364 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: let user-management menu pick and act on multiple users per…
2026-08-20 15:22:41 -04:00
Claude 4b0f453952 authelia: let user-management menu pick and act on multiple users per run
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.
2026-08-20 19:21:45 +00:00
Outis c3133711cc Merge pull request #363 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: fix uppercase-username stripping, add delete/service-access…
2026-08-20 15:18:12 -04:00
Claude 31cbe435bc authelia: fix uppercase-username stripping, add delete/service-access to user menu
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.
2026-08-20 19:02:37 +00:00
Outis 040b006457 Merge pull request #362 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia/mealie/actualbudget/gitea: no more silent no-op reruns
2026-08-20 14:51:13 -04:00
Claude accfd7a5bb authelia/mealie/actualbudget/gitea: no more silent no-op reruns
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
2026-08-20 18:49:43 +00:00
Outis 0d605cc8c7 Merge pull request #361 from outis1one/claude/gitea-standalone-setup-oxoi2e
authelia: show existing users before asking who to scope access to
2026-08-20 14:44:43 -04:00
Claude 15cef3ab1d authelia: show existing users before asking who to scope access to
_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
2026-08-20 18:43:13 +00:00
Outis 60b5dd5f29 Merge pull request #360 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
2026-08-20 11:40:40 -04:00
Claude 2b6c06e060 authelia: automate "remember me" duration, fix stale config key in docs
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
2026-08-19 20:37:09 +00:00
Claude 699be5b4e3 authelia: make OIDC client registration self-healing on a stale client_id
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
2026-08-19 19:11:34 +00:00
Claude 3dca8dce1d mealie, actualbudget: add native "Sign in with Authelia" (OIDC)
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
2026-08-19 18:44:00 +00:00
Claude ab6b554cd8 authelia: add reusable per-service access scoping (universal vs specific users)
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
2026-08-19 18:29:36 +00:00
Claude 393f97d164 Merge branch 'claude/ionos-script-integration-x32ofw' into main
Add PJSIP transport-tls keep_alive_interval to mitigate IONOS WiFi
client disconnects.
2026-08-19 02:56:49 +00:00
Claude a12aae7819 asterisk: add PJSIP transport-tls keep_alive_interval for IONOS WiFi drops
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.
2026-08-19 02:56:34 +00:00
Claude d3d2a411b0 gitea: add optional Gitea Actions (CI) with a local runner
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
2026-08-17 21:32:35 +00:00
Claude deb38b3b1d gitea: add optional "Sign in with Authelia" (OIDC) to the login page
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
2026-08-17 18:54:41 +00:00