13 Commits

Author SHA1 Message Date
librelad
6813621fe9 config: enable multiple instances everywhere it is actually possible
Three apps were instanceable and the rest were silent, so the feature
looked far narrower than it is. Every app has now been checked against
the two rules instance_create.sh enforces, and the answer recorded in
its config rather than left unset.

32 apps are instance-safe and now say so. Six are not, and each says why
in its own words instead of being indistinguishable from an app nobody
had reviewed:

  pihole      a DNS server must own port 53
  unbound     a resolver must own its fixed 5335
  stalwart    a mail server must own 25/465/587/993
  traefik     must own 443, and one Traefik routes every other app
  prometheus  node-exporter and cadvisor carry no "prometheus" prefix
  stoat       pins 7881, and database/redis/rabbit/minio carry no prefix

The first four are genuinely one-per-host: the port is not arbitrary, it
is the protocol. The last two are compose-identity problems and could be
fixed by prefixing those service names, which is a change to make
deliberately rather than in passing.

Recorded as an explicit false with a reason, not left unset, so the next
person reads a decision instead of an absence. The audit was verified not
to pass anything vacuously: every app resolves at least one service name,
so no app reached "eligible" merely because nothing was found to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:38:34 +01:00
librelad
a96a3a69a1 fix(linkding): declare the admin keys its auth adapter writes
linkding_auth.sh persists ADMIN_USER and ADMIN_PASSWORD when the first admin is
created, and keeps the password in step on later resets of that account, but
linkding.config declared neither — so both writes were no-ops and the WebUI
credentials card never had anything to show. Predates the slot work; it only
became visible once authPersistCfg started warning instead of failing silently.

Added empty rather than RANDOMIZED*, because unlike bookstack or nextcloud
nothing seeds a linkding account at install — the first user is created from the
WebUI. A generated password would name an account that does not exist, and the
card would display a password that cannot log in. Unslotted for the same reason:
the slot number marks a value the installer generates, and this one is written at
runtime by the tool.

No AUTH_PROFILE key: nothing reads it (it exists only in a comment in
auth_adapter.sh), and adding an unread key is what was just cleaned up elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:24:28 +01:00
librelad
cdeb2d1658 feat(updater): per-app UPDATE_TYPE, automatic by default
Adds the decision half of the app updater. Detection (P2) and the
snapshot-first apply/revert (P3) were already real, but nothing ever
pressed the button — every update waited for a click.

  CFG_<APP>_UPDATE_TYPE=auto|manual   per app, default auto (33 templates)
  CFG_UPDATER_AUTO=true|false         master switch, default true

updaterAppPolicy resolves the two the way backupResolveStrategy already
resolves backup strategy: the global switch can only make things more
manual. updaterApplyAuto runs at the end of `updater check` and enqueues
the ordinary updater_apply task for each auto app that has an update —
never applies inline, so an automatic update is the same code path, task
log, History entry and Roll back button as a manual one.

Safety: each attempt stamps its target digest under generated/auto/, so a
build that fails is rolled back and then left alone rather than retried on
every scan; in-flight updater tasks are skipped so scans can't stack.

Tracked end to end: updates.json carries each app's resolved update_type,
History entries carry trigger=manual|auto. The WebUI says whether updates
install themselves, chips only the apps that opted out, labels automatic
history, and — since an auto app's pending update needs no decision — keeps
it off the Overview board's "Needs action" view.

Also fixes artifactApplyAuto enqueueing without --detach: it runs inside
the single-threaded task processor's own poll, so following the new task in
the foreground waits for a task that cannot start until it returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:22:04 +01:00
librelad
56102ac5c0 feat(updater): P1b — CFG_<APP>_VERSION drives the anchor image tag
Add a `#LIBREPORTAL|<APP>_VERSION_TAG|<current-tag>` sentinel to each app's
anchor (`<slug>-service`) image line, so a `CFG_<APP>_VERSION` config drives
that tag through the existing tagging system — the config becomes the source,
the compose tag is derived (like ports/IPs/domains already are). This is the
"install/pin a specific version" knob.

- Placeholder is the literal current tag (e.g. `31-fpm-alpine`), NOT a `*_DATA`
  token: an unset var leaves the line untouched (inert comment, keeps the real
  tag) and never trips the up_app stale-tag scanner.
- Only the anchor line is tagged (sidecars mariadb/redis/nginx stay
  tracked-by-digest, not user-version-picked). ollama correctly targets
  `ollama/ollama`, not the companion open-webui.
- Untagged anchors normalized to `:latest` (semantic no-op) so they're
  templatable too. The manager (libreportal) is skipped — it updates via its
  release channel, not a docker tag.

32 apps wired. Verified: CFG_NEXTCLOUD_VERSION=32 → `nextcloud:32` (prefix
preserved, idempotent, sidecars untouched); all compose files still valid YAML.
Inert until a version config is set, so no behaviour change on existing installs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-17 23:13:57 +01:00
librelad
61b40c96aa copy(tools): shorter, jargon-free descriptions across all per-app tools
Tool descriptions were leaking internal vocabulary (Django superuser,
Postgres bcrypt update, htpasswd in protectionauth.yml, gitea admin
user change-password CLI, trusted_domains list, …) and repeating the
label as a full sentence. Beginners don't care, and even experienced
users don't need the CLI name to know what a button does.

Rewrites every tool description to a single short sentence plain
enough that a first-time installer can read it without context.

Conventions applied across the board:
  - One sentence, sentence-case
  - Plain English: "Set a new password", "Add a new user",
    "Permanently remove a user", "List every user"
  - "Leave blank to generate one" only where it's actually useful
    (password fields), and matches the field placeholder text
  - No CLI names, no schema field names, no internal file paths
  - Destructive actions stop saying "permanently" twice (the action
    label + the confirm modal already cover that)
  - Field placeholders harmonised: "Leave blank for random" /
    "Leave blank to generate" → consistently "Leave blank to generate"

Touched files (descriptions only — no logic, no fields removed):
  containers/adguard/tools/adguard.tools.json
  containers/bookstack/tools/bookstack.tools.json
  containers/dashy/tools/dashy.tools.json
  containers/focalboard/tools/focalboard.tools.json
  containers/gitea/tools/gitea.tools.json
  containers/gluetun/tools/gluetun.tools.json
  containers/invidious/tools/invidious.tools.json
  containers/linkding/tools/linkding.tools.json
  containers/nextcloud/tools/nextcloud.tools.json
  containers/pihole/tools/pihole.tools.json
  containers/traefik/tools/traefik.tools.json

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:27:53 +01:00
librelad
d941f59388 feat(app): generic installApp driver + dispatcher fallback (Wave A)
The 31 containers/<app>/<app>.sh files each defined install<App>() with
the SAME 10-step sequence — ~4,000 lines of duplicated boilerplate.
Replaces all that with one generic driver + hook surface.

scripts/app/install/app_install.sh:
  installApp <slug> [config_variables]
    — Dispatches on $<slug> (c/u/s/r/i) the same way the per-app .sh
      files did. Same convention; dockerInstallApp's existing
      `declare $app=i` callsite needs no change.
    — Runs the standard sequence: dockerConfigSetupToContainer →
      dockerComposeSetupFile → optional .env copy → fixPermissions →
      dockerComposeUpdateAndStartApp → standard post-install steps
      (appUpdateSpecifics, setupHeadscale, databaseInstallApp,
      webuiContainerSetup, monitoring registration) → final message.
    — Hooks (all declare-f-gated, silent no-op when absent):
        <slug>_install_pre / _post_setup / _post_compose / _post_start
        <slug>_install_message_data   (echoes extra args for menu)
        <slug>_install_post
        <slug>_uninstall_pre / _post
        <slug>_stop_post
        <slug>_restart_post
      Hooks live in containers/<app>/tools/<app>_tools.sh (auto-sourced
      per the modular-per-app-tools convention).

function_install_app.sh:
  When no install<App>() function exists, fall through to
  `installApp <app_name>` instead of erroring. So an app with no .sh
  at all becomes a zero-byte addition — drop in <app>.config +
  docker-compose.yml + <app>.svg, done.

containers/linkding/linkding.sh:
  Deleted (canary). Linkding's body was 100% standard sequence;
  fallback handles it identically. Smoke-tested with stubbed helpers
  — dispatcher fires, generic runs full flow, monitoring integration
  + final-message hook plumbing all intact.

Wave B (next): delete the .sh for every other 'pure-boilerplate' app
(~15 candidates per the survey). Wave C: extract custom logic from
the 7 fat apps into hooks before deleting their .sh.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:43:08 +01:00
librelad
dc77ddaa4c feat(linkding): add full per-app tools (5 user-management actions)
Linkding has shipped without any Tools-tab actions since v0.1.0 — the only
artifact was scripts/menu/tools/manage_linkding.sh, a dead legacy CLI menu
referencing an `appLinkdingSetupUser` function that was never defined.
Build the real thing, mirroring bookstack's pattern (manifest + thin tool
wrappers + auth_adapter that drives the app's native admin shell):

  containers/linkding/tools/linkding.tools.json    — manifest, 5 tools
  containers/linkding/tools/linkding_<id>.sh       — one wrapper per tool
  containers/linkding/scripts/linkding_auth.sh     — Django shell driver

Tools (all category=users, so the WebUI's custom user-list panel and its
row-level 🔑 / 👑 / 🗑 buttons light up):

  reset_password   — set_password on an existing user (random if blank)
  create_account   — create_user / create_superuser
  list_users       — emits EZ_USER\t<username>\t<username>\t<role> rows
                     (linkding is username-primary, so username goes into
                     both display slots — keeps the panel click-through
                     identifier consistent with the other tools' fields)
  delete_user      — delete by username (destructive, confirm gated)
  set_admin        — toggle is_superuser + is_staff

Implementation runs entirely inside the linkding-service container via
`runFileOp docker exec ... python manage.py shell -c "<code>"`, reading
inputs through `-e` env vars so quoting stays safe. Django's default
get_user_model() User is used directly — passwords hash exactly the way
the web UI does, admin flags map to the same fields the UI reads.

Also drop the dead legacy stub (scripts/menu/tools/manage_linkding.sh)
and regenerate files_menu.sh so the source-scan no longer pulls it in.
Nothing referenced linkdingToolsMenu — verified by tree-wide grep.

Verified live on dev-ai (Debian 12, linkding installed, Django 5 + sqlite):

  $ libreportal app tool linkding create_account 'username=alice|password=…|admin=true'
  ✓ Linkding user created — Username: alice — Password: …

  $ libreportal app tool linkding list_users ''
  EZ_USER  alice  alice  admin

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 20:23:56 +01:00
librelad
12b4d6823e feat(backup): file-capture labels for linkding, vaultwarden, headscale, mastodon
Now that uid auto-discover is in (d424473), adding more apps is just naming the
container + path — no uid guessing. Four more apps get complete backups of their
private data dirs (previously: incomplete on the file side because libreportal
can't read sub-UID-owned content from the host).

- linkding-service     -> /etc/linkding/data           (overlaps with sqlite db, same pattern as gitea — dump replays over the captured tree on restore, harmless)
- vaultwarden-service  -> /data                         (same overlap pattern)
- headscale-service    -> /var/lib/headscale            (same overlap pattern)
- mastodon-service     -> /mastodon/public/system       (uploads; postgres handled separately by backup.db)

Coverage now: nextcloud, bookstack, gitea, owncloud, linkding, vaultwarden,
headscale, mastodon. Skipped jellyfin — it has multiple internal sqlite DBs and
no backup.db declared; adding just backup.files without backup.db / backup.live
wouldn't activate live capture, and adding backup.live blind could yield torn
sqlites. That one wants proper DB declaration first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 16:13:46 +01:00
librelad
27ad517626 feat(backup): per-app strategy override (advanced, context-aware)
Adds CFG_<APP>_BACKUP_STRATEGY (default auto) so an app's backup strategy can
be overridden from its Advanced config tab, taking precedence over the global
default. Added to the 10 live-capable apps, so the dropdown's 'live' option only
appears where it actually works.

- backupResolveStrategy now checks the per-app override before the global value.
- backupAppLiveCapable / backupAppStrategyOptions expose capability + the valid
  option set; predicate helpers hardened with explicit returns so they behave
  identically with or without shell errexit.
- BACKUP_STRATEGY field mapping (select, advanced) renders the dropdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:34:17 +01:00
librelad
d97a09b119 feat(backup): declare sqlite databases for live backup
Add libreportal.backup.db labels for the SQLite apps with confirmed db paths:
vaultwarden, linkding, trilium, headscale, authelia. These are dumped live via
sqlite3 .backup and rehydrated before start on restore.

gitea and focalboard are intentionally left out until their sqlite paths are
confirmed on a live install — a wrong path would just fall back to stop, but
there's no point shipping a descriptor that always falls back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:16:05 +01:00
librelad
2e4f4202e1 refactor(routing): retire HOST_NAME — derive primary host from per-port subdomains
The static per-app CFG_<APP>_HOST_NAME is gone. host_setup (the app's
canonical FQDN, feeding the legacy single DOMAINSUBNAME_DATA used by app env
vars, the app URL and trusted-domains) is now derived from the app's primary
Traefik port's subdomain: first recommended port, else first Traefik port;
@/root -> apex, set -> sub.domain, empty -> app-name. Removes HOST_NAME from
all app configs, the config-form field mapping (Hostname), the dead
headscale stub, and wireguard.sh (now uses host_setup). Completes the move to
dynamic per-port subdomain routing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 11:25:00 +01:00
librelad
dec3055b63 feat(routing): dynamic per-port subdomains + router-block toggle
Replace the static one-host-per-app model with per-port routers: each
Traefik-managed port carries a subdomain (12-col PORT format) and gets a
DOMAINSUBNAME_TAG_<n> host, so one container can serve unlimited hosts.
tagsProcessorPortSubdomains stamps per-port hosts (subdomain @/empty = apex,
multi-level allowed); tagsProcessorPortRouterBlocks comments out
# TRAEFIK_PORT_<n>_BEGIN/END blocks for non-Traefik ports so unfilled
placeholders never ship (mirrors GLUETUN_OFF). Convert all 27 router apps
(subdomains seeded from HOST_NAME; headscale admin. prefix -> subdomain).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 00:45:01 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00