diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css index 1b67208..01fea2c 100644 --- a/containers/libreportal/frontend/components/apps/core/css/apps.css +++ b/containers/libreportal/frontend/components/apps/core/css/apps.css @@ -542,6 +542,24 @@ border: 1px solid var(--border-subtle, var(--border-color)); border-radius: 12px; } +/* The "not available" variant of the bar: same frame, quieter, since it states a + fact rather than offering an action. */ +.instance-family-bar-off { + background: transparent; + border-style: dashed; +} +.instance-family-note { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.45; +} +.instance-family-note code { + font-size: 12px; + padding: 1px 5px; + border-radius: 5px; + background: rgba(var(--text-rgb), 0.08); + color: var(--text-primary); +} .instance-family-label { font-size: 11px; font-weight: 700; diff --git a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js index ab09c95..fd78624 100644 --- a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js +++ b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js @@ -331,7 +331,41 @@ Object.assign(AppsManager.prototype, { const type = isInstance ? myCfg[`CFG_${U(cleanAppName)}_INSTANCE_OF`] : cleanAppName; const typeCfg = cfgOf(type); const isCapable = String(typeCfg[`CFG_${U(type)}_MULTI_INSTANCE`]).toLowerCase() === 'true'; - if (!isCapable) return ''; + + const typeAppEarly = apps.find(x => slugOf(x) === type); + const typeTitleEarly = typeAppEarly ? (typeAppEarly.name || type).split(' - ')[0].trim() : type; + + // Not instanceable: say so where the pills would have been, rather than + // rendering nothing. An empty space reads as "this page has no instance + // feature", which sends people looking for a setting that isn't missing. + // + // Where the reason is knowable from config, give it. A pinned host port is + // the common blocker and the only one visible client-side: the port rows are + // right here, so this mirrors _instanceCheckPortsInstanceable exactly — + // skip `disabled` rows (they publish nothing) and `random` ones (already + // per-instance). Anything else is left unexplained rather than guessed at, + // since the remaining blocker lives in the compose, which the frontend + // never sees. + if (!isCapable) { + const pinned = []; + for (let i = 1; i <= 20; i++) { + const row = typeCfg[`CFG_${U(type)}_PORT_${i}`]; + if (!row) continue; + const f = String(row).split('|'); + const ext = (f[2] || '').split(':')[0]; + if (!ext || ext === 'random' || f[3] === 'disabled') continue; + if (!pinned.includes(ext)) pinned.push(ext); + } + const why = pinned.length + ? `${esc(typeTitleEarly)} publishes on fixed host port${pinned.length > 1 ? 's' : ''} ` + + `${esc(pinned.join(', '))}, which a second copy could not bind.` + : `${esc(typeTitleEarly)} isn't enabled for multiple instances.`; + return ` +
+ Instances + Not available — ${why} +
`; + } const instances = apps .map(slugOf) diff --git a/containers/searxng/searxng.config b/containers/searxng/searxng.config index 7328ec8..0390281 100755 --- a/containers/searxng/searxng.config +++ b/containers/searxng/searxng.config @@ -11,6 +11,11 @@ # HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # CFG_SEARXNG_APP_NAME=searxng +# MULTI_INSTANCE = if true, this app can run as multiple isolated instances +# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on +# apps whose compose identity (container_name, Traefik routers, backup labels) +# is instance-safe — see scripts/instance/instance_create.sh. +CFG_SEARXNG_MULTI_INSTANCE=true CFG_SEARXNG_BACKUP=false CFG_SEARXNG_BACKUP_STRATEGY=auto CFG_SEARXNG_UPDATE_TYPE=auto diff --git a/containers/vaultwarden/vaultwarden.config b/containers/vaultwarden/vaultwarden.config index e88c091..77c34d7 100755 --- a/containers/vaultwarden/vaultwarden.config +++ b/containers/vaultwarden/vaultwarden.config @@ -12,6 +12,11 @@ # MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed) # CFG_VAULTWARDEN_APP_NAME=vaultwarden +# MULTI_INSTANCE = if true, this app can run as multiple isolated instances +# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on +# apps whose compose identity (container_name, Traefik routers, backup labels) +# is instance-safe — see scripts/instance/instance_create.sh. +CFG_VAULTWARDEN_MULTI_INSTANCE=true CFG_VAULTWARDEN_BACKUP=true CFG_VAULTWARDEN_BACKUP_STRATEGY=auto CFG_VAULTWARDEN_UPDATE_TYPE=auto diff --git a/scripts/instance/instance_create.sh b/scripts/instance/instance_create.sh index 499c0fb..f9a860c 100644 --- a/scripts/instance/instance_create.sh +++ b/scripts/instance/instance_create.sh @@ -312,6 +312,8 @@ _instanceRewriteCompose() { _instanceRewriteTools() { local type="$1" slug="$2" dir="$3" src_compose="$4" local d f base + local type_u="${type^^}" slug_u="${slug^^}" + type_u="${type_u//-/_}"; slug_u="${slug_u//-/_}" # Container identities as they appear in hook/tool code. The per-token rename # below is what catches `docker exec -u git gitea-service …`: the docker-verb @@ -361,6 +363,24 @@ _instanceRewriteTools() { # names and prose. sed -i -E "s/(\b(container|container_name)=\")${type}(\")/\1${slug}\3/g" "$f" + # Function names carrying the app as a SUFFIX. The prefix rule above + # only matches _, so appSetupComposeTags_vaultwarden survived + # untouched — and docker_config_setup_data.sh dispatches that hook as + # appSetupComposeTags_${app_name}, i.e. ..._vaultwarden_work for an + # instance. The clone therefore defined a function nobody calls (under + # a name that collides with the base app's), its compose tags were + # never filled, and the pre-start guard refused to launch the + # instance. Anchored on the () of a definition so only real function + # names are touched. Affects 8 apps that ship this hook shape. + sed -i -E "s/\b([A-Za-z_][A-Za-z0-9_]*)_${type}(\(\))/\1_${slug}\2/g" "$f" + + # The uppercase tag namespace, mirroring rule 4 of the compose rewrite. + # These hooks pass tag NAMES as strings ("VAULTWARDEN_ADMIN_TOKEN_1_TAG"), + # which the lowercase renames above cannot see. The cloned compose has + # already moved to _..._TAG, so leaving these behind would update + # a tag that no longer exists in the file. + sed -i -E "s/\b${type_u}_([A-Z0-9_]*_TAG)\b/${slug_u}_\1/g" "$f" + # The app's own deployed directory. Hooks that build it from # "${containers_dir}/..." instead of "$containers_dir$app_name/..." # read and WRITE the base app's files — adguard's auth adapter edits