diff --git a/containers/adguard/adguard.config b/containers/adguard/adguard.config index 0216ff4..6c8ea98 100755 --- a/containers/adguard/adguard.config +++ b/containers/adguard/adguard.config @@ -21,6 +21,8 @@ CFG_ADGUARD_APP_NAME=adguard # Not instanced by design. A DNS server is what clients point at; a second copy on a different port resolves for nobody. One-per-host in practice, like pihole and unbound. CFG_ADGUARD_MULTI_INSTANCE=false CFG_ADGUARD_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_ADGUARD_STORAGE=default # Storage Location - Fixed: other apps reference adguard by path **READONLY** CFG_ADGUARD_BACKUP_STRATEGY=auto CFG_ADGUARD_UPDATE_TYPE=auto CFG_ADGUARD_COMPOSE_FILE=default diff --git a/containers/authelia/authelia.config b/containers/authelia/authelia.config index 94f025c..978d33b 100755 --- a/containers/authelia/authelia.config +++ b/containers/authelia/authelia.config @@ -24,6 +24,9 @@ CFG_AUTHELIA_APP_NAME=authelia CFG_AUTHELIA_MULTI_INSTANCE=false CFG_AUTHELIA_REQUIRES="domain,traefik" CFG_AUTHELIA_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move authelia ` — editing here records intent only. +CFG_AUTHELIA_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_AUTHELIA_BACKUP_STRATEGY=auto CFG_AUTHELIA_UPDATE_TYPE=auto CFG_AUTHELIA_COMPOSE_FILE=default diff --git a/containers/bookstack/bookstack.config b/containers/bookstack/bookstack.config index 09f9ad8..ee0dc3f 100644 --- a/containers/bookstack/bookstack.config +++ b/containers/bookstack/bookstack.config @@ -19,6 +19,9 @@ CFG_BOOKSTACK_APP_NAME=bookstack # is instance-safe — see scripts/instance/instance_create.sh. CFG_BOOKSTACK_MULTI_INSTANCE=true CFG_BOOKSTACK_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move bookstack ` — editing here records intent only. +CFG_BOOKSTACK_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_BOOKSTACK_BACKUP_STRATEGY=auto CFG_BOOKSTACK_UPDATE_TYPE=auto CFG_BOOKSTACK_COMPOSE_FILE=default diff --git a/containers/crowdsec/crowdsec.config b/containers/crowdsec/crowdsec.config index 8e50df5..c03cbbb 100644 --- a/containers/crowdsec/crowdsec.config +++ b/containers/crowdsec/crowdsec.config @@ -27,6 +27,8 @@ CFG_CROWDSEC_HOST_PACKAGE=crowdsec CFG_CROWDSEC_HOST_SERVICES=crowdsec.service,crowdsec-firewall-bouncer.service CFG_CROWDSEC_HOST_LOG_FILES="crowdsec.service|/var/log/crowdsec.log,crowdsec-firewall-bouncer.service|/var/log/crowdsec-firewall-bouncer.log" CFG_CROWDSEC_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_CROWDSEC_STORAGE=default # Storage Location - Fixed: other apps reference crowdsec by path **READONLY** CFG_CROWDSEC_BACKUP_STRATEGY=auto CFG_CROWDSEC_UPDATE_TYPE=auto CFG_CROWDSEC_MONITORING=false diff --git a/containers/dashy/dashy.config b/containers/dashy/dashy.config index 4c4b050..7a71817 100755 --- a/containers/dashy/dashy.config +++ b/containers/dashy/dashy.config @@ -17,6 +17,8 @@ CFG_DASHY_APP_NAME=dashy # is instance-safe — see scripts/instance/instance_create.sh. CFG_DASHY_MULTI_INSTANCE=true CFG_DASHY_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_DASHY_STORAGE=default # Storage Location - Fixed: other apps reference dashy by path **READONLY** CFG_DASHY_BACKUP_STRATEGY=auto CFG_DASHY_UPDATE_TYPE=auto CFG_DASHY_COMPOSE_FILE=default diff --git a/containers/gitea/gitea.config b/containers/gitea/gitea.config index f577e72..e59f314 100755 --- a/containers/gitea/gitea.config +++ b/containers/gitea/gitea.config @@ -19,6 +19,9 @@ CFG_GITEA_APP_NAME=gitea # is instance-safe — see scripts/instance/instance_create.sh. CFG_GITEA_MULTI_INSTANCE=true CFG_GITEA_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move gitea ` — editing here records intent only. +CFG_GITEA_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_GITEA_BACKUP_STRATEGY=auto CFG_GITEA_UPDATE_TYPE=auto CFG_GITEA_COMPOSE_FILE=default diff --git a/containers/gluetun/gluetun.config b/containers/gluetun/gluetun.config index bd53289..4166291 100644 --- a/containers/gluetun/gluetun.config +++ b/containers/gluetun/gluetun.config @@ -19,6 +19,8 @@ CFG_GLUETUN_APP_NAME=gluetun # Not instanced by design. A network provider, not a destination: other apps join it with network_mode "container:gluetun-service". A second copy leaves that name ambiguous. CFG_GLUETUN_MULTI_INSTANCE=false CFG_GLUETUN_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_GLUETUN_STORAGE=default # Storage Location - Fixed: other apps reference gluetun by path **READONLY** CFG_GLUETUN_BACKUP_STRATEGY=auto CFG_GLUETUN_UPDATE_TYPE=auto CFG_GLUETUN_COMPOSE_FILE=default diff --git a/containers/grafana/grafana.config b/containers/grafana/grafana.config index bb8cd80..811e408 100755 --- a/containers/grafana/grafana.config +++ b/containers/grafana/grafana.config @@ -20,6 +20,8 @@ CFG_GRAFANA_APP_NAME=grafana CFG_GRAFANA_MULTI_INSTANCE=true CFG_GRAFANA_REQUIRES="prometheus" CFG_GRAFANA_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_GRAFANA_STORAGE=default # Storage Location - Fixed: other apps reference grafana by path **READONLY** CFG_GRAFANA_BACKUP_STRATEGY=auto CFG_GRAFANA_UPDATE_TYPE=auto CFG_GRAFANA_COMPOSE_FILE=default diff --git a/containers/headscale/headscale.config b/containers/headscale/headscale.config index 8b3133d..c05592f 100755 --- a/containers/headscale/headscale.config +++ b/containers/headscale/headscale.config @@ -18,6 +18,8 @@ CFG_HEADSCALE_APP_NAME=headscale # Not instanced by design. The control server a tailnet is defined by — nodes register against exactly one. CFG_HEADSCALE_MULTI_INSTANCE=false CFG_HEADSCALE_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_HEADSCALE_STORAGE=default # Storage Location - Fixed: other apps reference headscale by path **READONLY** CFG_HEADSCALE_BACKUP_STRATEGY=auto CFG_HEADSCALE_UPDATE_TYPE=auto CFG_HEADSCALE_COMPOSE_FILE=default diff --git a/containers/invidious/invidious.config b/containers/invidious/invidious.config index 09a8fd8..c553e38 100755 --- a/containers/invidious/invidious.config +++ b/containers/invidious/invidious.config @@ -17,6 +17,9 @@ CFG_INVIDIOUS_APP_NAME=invidious # is instance-safe — see scripts/instance/instance_create.sh. CFG_INVIDIOUS_MULTI_INSTANCE=true CFG_INVIDIOUS_BACKUP=false +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move invidious ` — editing here records intent only. +CFG_INVIDIOUS_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_INVIDIOUS_BACKUP_STRATEGY=auto CFG_INVIDIOUS_UPDATE_TYPE=auto CFG_INVIDIOUS_COMPOSE_FILE=default diff --git a/containers/ipinfo/ipinfo.config b/containers/ipinfo/ipinfo.config index 8953d06..fdb5e78 100755 --- a/containers/ipinfo/ipinfo.config +++ b/containers/ipinfo/ipinfo.config @@ -17,6 +17,9 @@ CFG_IPINFO_APP_NAME=ipinfo # is instance-safe — see scripts/instance/instance_create.sh. CFG_IPINFO_MULTI_INSTANCE=true CFG_IPINFO_BACKUP=false +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move ipinfo ` — editing here records intent only. +CFG_IPINFO_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_IPINFO_BACKUP_STRATEGY=auto CFG_IPINFO_UPDATE_TYPE=auto CFG_IPINFO_COMPOSE_FILE=default diff --git a/containers/jellyfin/jellyfin.config b/containers/jellyfin/jellyfin.config index 0c9f79a..c2b7d3e 100755 --- a/containers/jellyfin/jellyfin.config +++ b/containers/jellyfin/jellyfin.config @@ -17,6 +17,9 @@ CFG_JELLYFIN_APP_NAME=jellyfin # is instance-safe — see scripts/instance/instance_create.sh. CFG_JELLYFIN_MULTI_INSTANCE=true CFG_JELLYFIN_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move jellyfin ` — editing here records intent only. +CFG_JELLYFIN_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_JELLYFIN_BACKUP_STRATEGY=auto CFG_JELLYFIN_UPDATE_TYPE=auto CFG_JELLYFIN_COMPOSE_FILE=default diff --git a/containers/jitsimeet/jitsimeet.config b/containers/jitsimeet/jitsimeet.config index e7a0242..d0576e6 100755 --- a/containers/jitsimeet/jitsimeet.config +++ b/containers/jitsimeet/jitsimeet.config @@ -17,6 +17,9 @@ CFG_JITSIMEET_APP_NAME=jitsimeet # is instance-safe — see scripts/instance/instance_create.sh. CFG_JITSIMEET_MULTI_INSTANCE=true CFG_JITSIMEET_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move jitsimeet ` — editing here records intent only. +CFG_JITSIMEET_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_JITSIMEET_BACKUP_STRATEGY=auto CFG_JITSIMEET_UPDATE_TYPE=auto CFG_JITSIMEET_COMPOSE_FILE=default diff --git a/containers/libreportal/frontend/components/admin/config/js/config-utils.js b/containers/libreportal/frontend/components/admin/config/js/config-utils.js index 436960d..81d5b02 100755 --- a/containers/libreportal/frontend/components/admin/config/js/config-utils.js +++ b/containers/libreportal/frontend/components/admin/config/js/config-utils.js @@ -20,6 +20,7 @@ class ConfigUtils { cleanDescription(description) { return description .replace(/\*\*ADVANCED\*\*/g, '') + .replace(/\*\*READONLY\*\*/g, '') .replace(/\*\*UNUSED\*\*/g, '') .replace(/\*\*DEV\*\*/g, '') .replace(/^\s+|\s+$/g, '') // Trim whitespace diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-format.js b/containers/libreportal/frontend/components/tasks/js/tasks-format.js index cbc7c88..0231850 100644 --- a/containers/libreportal/frontend/components/tasks/js/tasks-format.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-format.js @@ -66,6 +66,10 @@ Object.assign(TasksManager.prototype, { // one it collapsed to "Restart Application", losing the only detail that // distinguishes it from restarting the whole app. { match: /^libreportal app restart (\S+) (\S+)/, title: (m) => `${displayName(m[1])} - Restart ${m[2]}` }, + // Storage move — name the destination. The generic `app ` + // fallback below would render "Move Application" and silently drop the + // one detail that matters about the task. + { match: /^libreportal app move (\S+) (\S+)/, title: (m) => `${displayName(m[1])} - Move to ${m[2]}` }, // -- Instances --------------------------------------------------------- // Named off the TYPE, not the new slug: at create time the instance does @@ -162,6 +166,7 @@ Object.assign(TasksManager.prototype, { 'rebuild': 'Rebuild Application', 'delete': 'Delete Backup', 'backup': 'Backup Application', + 'move': 'Move Application', // Compose verbs the WebUI dispatches. Without these the generic // " Application" fallback rendered "Up Application" and // "Down Application", which read as broken English rather than a task. diff --git a/containers/libreportal/frontend/core/config/js/field-factory.js b/containers/libreportal/frontend/core/config/js/field-factory.js index 6fe0639..b929401 100755 --- a/containers/libreportal/frontend/core/config/js/field-factory.js +++ b/containers/libreportal/frontend/core/config/js/field-factory.js @@ -523,7 +523,41 @@ class ConfigShared { } // Generate appropriate field based on value type and key + // A field whose comment carries **READONLY** is shown but not editable. + // + // "Pinned" is not a fact about a value, it is a statement about whether the + // field may be changed — so it belongs in the comment, beside **ADVANCED** + // and **DEV**, rather than in a second config key. Used by derived values the + // user should SEE but never type over: a storage location's registry-owned + // path, and CFG__STORAGE for infrastructure apps that other apps reach + // by path. Before this, such fields were editable inputs with prose telling + // you editing them does nothing. + static isReadonlyField(description) { + return typeof description === 'string' && description.includes('**READONLY**'); + } + + static generateReadonlyField(fieldId, key, value, title, description) { + const clean = (window.ConfigUtils && window.ConfigUtils.cleanDescription) + ? window.ConfigUtils.cleanDescription(description) + : String(description || '').replace(/\*\*[A-Z]+\*\*/g, '').trim(); + const safe = String(value == null ? '' : value); + return ` +
+ + + ${clean ? `${clean}` : ''} +
+ `; + } + static generateField(fieldId, key, value, title, description, options = {}, allConfig = {}) { + if (this.isReadonlyField(description)) { + return this.generateReadonlyField(fieldId, key, value, title, description); + } + // Note: Git fields (CFG_GIT_*) are now handled by the toggle system in renderGitSection // They don't need to be hidden here since the section itself is toggled diff --git a/containers/libreportal/frontend/core/tasks/js/task-commands.js b/containers/libreportal/frontend/core/tasks/js/task-commands.js index cd51510..acf528b 100755 --- a/containers/libreportal/frontend/core/tasks/js/task-commands.js +++ b/containers/libreportal/frontend/core/tasks/js/task-commands.js @@ -15,6 +15,8 @@ class TaskCommands { start: 'libreportal app start {appName}', stop: 'libreportal app stop {appName}', backup: 'libreportal app backup {appName}', + // Storage location move — see scripts/storage/storage_move.sh. + move: 'libreportal app move {appName} {location}', status: 'libreportal app status {appName}', // Multi-instance (✅ IMPLEMENTED) — provision another isolated copy of a @@ -51,6 +53,7 @@ class TaskCommands { start: 'implemented', stop: 'implemented', backup: 'implemented', + move: 'implemented', status: 'implemented', up: 'implemented', down: 'implemented', @@ -146,6 +149,7 @@ class TaskCommands { start: ['appName'], stop: ['appName'], backup: ['appName'], + move: ['appName', 'location'], status: ['appName'], up: ['appName'], down: ['appName'], diff --git a/containers/linkding/linkding.config b/containers/linkding/linkding.config index 963efd3..36ad2d9 100755 --- a/containers/linkding/linkding.config +++ b/containers/linkding/linkding.config @@ -17,6 +17,9 @@ CFG_LINKDING_APP_NAME=linkding # is instance-safe — see scripts/instance/instance_create.sh. CFG_LINKDING_MULTI_INSTANCE=true CFG_LINKDING_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move linkding ` — editing here records intent only. +CFG_LINKDING_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_LINKDING_BACKUP_STRATEGY=auto CFG_LINKDING_UPDATE_TYPE=auto CFG_LINKDING_COMPOSE_FILE=default diff --git a/containers/mastodon/mastodon.config b/containers/mastodon/mastodon.config index 8808160..9aa7625 100755 --- a/containers/mastodon/mastodon.config +++ b/containers/mastodon/mastodon.config @@ -17,6 +17,9 @@ CFG_MASTODON_APP_NAME=mastodon # is instance-safe — see scripts/instance/instance_create.sh. CFG_MASTODON_MULTI_INSTANCE=true CFG_MASTODON_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move mastodon ` — editing here records intent only. +CFG_MASTODON_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_MASTODON_BACKUP_STRATEGY=auto CFG_MASTODON_UPDATE_TYPE=auto CFG_MASTODON_COMPOSE_FILE=default diff --git a/containers/matrix/matrix.config b/containers/matrix/matrix.config index 74c5960..0b1e1ec 100644 --- a/containers/matrix/matrix.config +++ b/containers/matrix/matrix.config @@ -28,6 +28,9 @@ CFG_MATRIX_MULTI_INSTANCE=true # with public DNS and TLS, and federation is optional. CFG_MATRIX_REQUIRES="" CFG_MATRIX_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move matrix ` — editing here records intent only. +CFG_MATRIX_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_MATRIX_BACKUP_STRATEGY=auto CFG_MATRIX_UPDATE_TYPE=auto CFG_MATRIX_COMPOSE_FILE=default diff --git a/containers/mattermost/mattermost.config b/containers/mattermost/mattermost.config index 5c56c88..d73a3ea 100644 --- a/containers/mattermost/mattermost.config +++ b/containers/mattermost/mattermost.config @@ -18,6 +18,9 @@ CFG_MATTERMOST_APP_NAME=mattermost # is instance-safe — see scripts/instance/instance_create.sh. CFG_MATTERMOST_MULTI_INSTANCE=true CFG_MATTERMOST_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move mattermost ` — editing here records intent only. +CFG_MATTERMOST_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_MATTERMOST_BACKUP_STRATEGY=auto CFG_MATTERMOST_UPDATE_TYPE=auto CFG_MATTERMOST_COMPOSE_FILE=default diff --git a/containers/moneyapp/moneyapp.config b/containers/moneyapp/moneyapp.config index cf0e642..129d40c 100644 --- a/containers/moneyapp/moneyapp.config +++ b/containers/moneyapp/moneyapp.config @@ -19,6 +19,9 @@ CFG_MONEYAPP_APP_NAME=moneyapp CFG_MONEYAPP_MULTI_INSTANCE=true CFG_MONEYAPP_REQUIRES="" CFG_MONEYAPP_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move moneyapp ` — editing here records intent only. +CFG_MONEYAPP_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_MONEYAPP_BACKUP_STRATEGY=auto CFG_MONEYAPP_UPDATE_TYPE=auto CFG_MONEYAPP_COMPOSE_FILE=default diff --git a/containers/navidrome/navidrome.config b/containers/navidrome/navidrome.config index eaf62ef..c8a25c2 100644 --- a/containers/navidrome/navidrome.config +++ b/containers/navidrome/navidrome.config @@ -17,6 +17,9 @@ CFG_NAVIDROME_APP_NAME=navidrome # is instance-safe — see scripts/instance/instance_create.sh. CFG_NAVIDROME_MULTI_INSTANCE=true CFG_NAVIDROME_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move navidrome ` — editing here records intent only. +CFG_NAVIDROME_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_NAVIDROME_BACKUP_STRATEGY=auto CFG_NAVIDROME_UPDATE_TYPE=auto CFG_NAVIDROME_COMPOSE_FILE=default diff --git a/containers/nextcloud/nextcloud.config b/containers/nextcloud/nextcloud.config index e9ef4ac..de11d3b 100755 --- a/containers/nextcloud/nextcloud.config +++ b/containers/nextcloud/nextcloud.config @@ -21,6 +21,9 @@ CFG_NEXTCLOUD_APP_NAME=nextcloud # is instance-safe — see scripts/instance/instance_create.sh. CFG_NEXTCLOUD_MULTI_INSTANCE=true CFG_NEXTCLOUD_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move nextcloud ` — editing here records intent only. +CFG_NEXTCLOUD_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_NEXTCLOUD_BACKUP_STRATEGY=auto CFG_NEXTCLOUD_UPDATE_TYPE=auto CFG_NEXTCLOUD_COMPOSE_FILE=default diff --git a/containers/ollama/ollama.config b/containers/ollama/ollama.config index 744126d..e6e8b51 100755 --- a/containers/ollama/ollama.config +++ b/containers/ollama/ollama.config @@ -19,6 +19,9 @@ CFG_OLLAMA_APP_NAME=ollama # Not instanced by design. Clients are configured against one endpoint, and each copy duplicates multi-gigabyte model storage for no routing benefit. CFG_OLLAMA_MULTI_INSTANCE=false CFG_OLLAMA_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move ollama ` — editing here records intent only. +CFG_OLLAMA_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_OLLAMA_BACKUP_STRATEGY=auto CFG_OLLAMA_UPDATE_TYPE=auto CFG_OLLAMA_COMPOSE_FILE=default diff --git a/containers/onlyoffice/onlyoffice.config b/containers/onlyoffice/onlyoffice.config index 77f1a4e..9a8f1e0 100755 --- a/containers/onlyoffice/onlyoffice.config +++ b/containers/onlyoffice/onlyoffice.config @@ -17,6 +17,9 @@ CFG_ONLYOFFICE_APP_NAME=onlyoffice # is instance-safe — see scripts/instance/instance_create.sh. CFG_ONLYOFFICE_MULTI_INSTANCE=true CFG_ONLYOFFICE_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move onlyoffice ` — editing here records intent only. +CFG_ONLYOFFICE_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_ONLYOFFICE_BACKUP_STRATEGY=auto CFG_ONLYOFFICE_UPDATE_TYPE=auto CFG_ONLYOFFICE_COMPOSE_FILE=default diff --git a/containers/owncloud/owncloud.config b/containers/owncloud/owncloud.config index 11baf20..e0a4eda 100755 --- a/containers/owncloud/owncloud.config +++ b/containers/owncloud/owncloud.config @@ -17,6 +17,9 @@ CFG_OWNCLOUD_APP_NAME=owncloud # is instance-safe — see scripts/instance/instance_create.sh. CFG_OWNCLOUD_MULTI_INSTANCE=true CFG_OWNCLOUD_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move owncloud ` — editing here records intent only. +CFG_OWNCLOUD_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_OWNCLOUD_BACKUP_STRATEGY=auto CFG_OWNCLOUD_UPDATE_TYPE=auto CFG_OWNCLOUD_COMPOSE_FILE=default diff --git a/containers/pihole/pihole.config b/containers/pihole/pihole.config index 458461e..ab3572e 100755 --- a/containers/pihole/pihole.config +++ b/containers/pihole/pihole.config @@ -19,6 +19,8 @@ CFG_PIHOLE_APP_NAME=pihole # Not instance-safe. A DNS server must own port 53 on the host; a second copy could not bind it. CFG_PIHOLE_MULTI_INSTANCE=false CFG_PIHOLE_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_PIHOLE_STORAGE=default # Storage Location - Fixed: other apps reference pihole by path **READONLY** CFG_PIHOLE_BACKUP_STRATEGY=auto CFG_PIHOLE_UPDATE_TYPE=auto CFG_PIHOLE_COMPOSE_FILE=default diff --git a/containers/prometheus/prometheus.config b/containers/prometheus/prometheus.config index 799c7f2..7e73a6f 100755 --- a/containers/prometheus/prometheus.config +++ b/containers/prometheus/prometheus.config @@ -17,6 +17,8 @@ CFG_PROMETHEUS_APP_NAME=prometheus # is instance-safe — see scripts/instance/instance_create.sh. CFG_PROMETHEUS_MULTI_INSTANCE=true CFG_PROMETHEUS_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_PROMETHEUS_STORAGE=default # Storage Location - Fixed: other apps reference prometheus by path **READONLY** CFG_PROMETHEUS_BACKUP_STRATEGY=auto CFG_PROMETHEUS_UPDATE_TYPE=auto CFG_PROMETHEUS_COMPOSE_FILE=default diff --git a/containers/rocketchat/rocketchat.config b/containers/rocketchat/rocketchat.config index c0297d7..e8ad228 100644 --- a/containers/rocketchat/rocketchat.config +++ b/containers/rocketchat/rocketchat.config @@ -18,6 +18,9 @@ CFG_ROCKETCHAT_APP_NAME=rocketchat # is instance-safe — see scripts/instance/instance_create.sh. CFG_ROCKETCHAT_MULTI_INSTANCE=true CFG_ROCKETCHAT_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move rocketchat ` — editing here records intent only. +CFG_ROCKETCHAT_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_ROCKETCHAT_BACKUP_STRATEGY=auto # Auto. Rocket.Chat does run schema migrations on boot and does refuse to start # too far behind its database — but an automatic update cannot put it there. diff --git a/containers/searxng/searxng.config b/containers/searxng/searxng.config index 0390281..773e0f4 100755 --- a/containers/searxng/searxng.config +++ b/containers/searxng/searxng.config @@ -17,6 +17,9 @@ CFG_SEARXNG_APP_NAME=searxng # is instance-safe — see scripts/instance/instance_create.sh. CFG_SEARXNG_MULTI_INSTANCE=true CFG_SEARXNG_BACKUP=false +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move searxng ` — editing here records intent only. +CFG_SEARXNG_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_SEARXNG_BACKUP_STRATEGY=auto CFG_SEARXNG_UPDATE_TYPE=auto CFG_SEARXNG_COMPOSE_FILE=default diff --git a/containers/speedtest/speedtest.config b/containers/speedtest/speedtest.config index 11549cc..dd99355 100755 --- a/containers/speedtest/speedtest.config +++ b/containers/speedtest/speedtest.config @@ -17,6 +17,9 @@ CFG_SPEEDTEST_APP_NAME=speedtest # is instance-safe — see scripts/instance/instance_create.sh. CFG_SPEEDTEST_MULTI_INSTANCE=true CFG_SPEEDTEST_BACKUP=false +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move speedtest ` — editing here records intent only. +CFG_SPEEDTEST_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_SPEEDTEST_BACKUP_STRATEGY=auto CFG_SPEEDTEST_UPDATE_TYPE=auto CFG_SPEEDTEST_COMPOSE_FILE=default diff --git a/containers/stalwart/stalwart.config b/containers/stalwart/stalwart.config index dbe45bf..bb0ff07 100644 --- a/containers/stalwart/stalwart.config +++ b/containers/stalwart/stalwart.config @@ -27,6 +27,9 @@ CFG_STALWART_APP_NAME=stalwart # Not instance-safe. A mail server must own the SMTP/IMAP ports (25, 465, 587, 993); a second copy could not bind them. CFG_STALWART_MULTI_INSTANCE=false CFG_STALWART_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move stalwart ` — editing here records intent only. +CFG_STALWART_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] # Mail is the one app where a "live" snapshot is a real risk: the message store # is being written continuously, so a copy taken mid-write can land mid-transaction. # stop-snapshot-start costs seconds of queued delivery (senders retry — that is diff --git a/containers/stoat/stoat.config b/containers/stoat/stoat.config index b5879a2..4bd5da3 100644 --- a/containers/stoat/stoat.config +++ b/containers/stoat/stoat.config @@ -33,6 +33,9 @@ CFG_STOAT_MULTI_INSTANCE=false # scheme. Any HTTPS satisfies it, including a self-signed Traefik certificate. CFG_STOAT_REQUIRES="" CFG_STOAT_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move stoat ` — editing here records intent only. +CFG_STOAT_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_STOAT_BACKUP_STRATEGY=auto # Auto. The components of this stack are released together and do expect # matching versions — which is exactly why they are all pinned to the same tag diff --git a/containers/traefik/traefik.config b/containers/traefik/traefik.config index a78d58b..4a249df 100755 --- a/containers/traefik/traefik.config +++ b/containers/traefik/traefik.config @@ -19,6 +19,8 @@ CFG_TRAEFIK_APP_NAME=traefik # Not instance-safe. The edge proxy must own 443 — and one Traefik is what routes every other app. CFG_TRAEFIK_MULTI_INSTANCE=false CFG_TRAEFIK_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_TRAEFIK_STORAGE=default # Storage Location - Fixed: other apps reference traefik by path **READONLY** CFG_TRAEFIK_BACKUP_STRATEGY=auto CFG_TRAEFIK_UPDATE_TYPE=auto CFG_TRAEFIK_COMPOSE_FILE=default diff --git a/containers/trilium/trilium.config b/containers/trilium/trilium.config index 5a18ec1..382b1da 100755 --- a/containers/trilium/trilium.config +++ b/containers/trilium/trilium.config @@ -17,6 +17,9 @@ CFG_TRILIUM_APP_NAME=trilium # is instance-safe — see scripts/instance/instance_create.sh. CFG_TRILIUM_MULTI_INSTANCE=true CFG_TRILIUM_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move trilium ` — editing here records intent only. +CFG_TRILIUM_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_TRILIUM_BACKUP_STRATEGY=auto CFG_TRILIUM_UPDATE_TYPE=auto CFG_TRILIUM_COMPOSE_FILE=default diff --git a/containers/trivy/trivy.config b/containers/trivy/trivy.config index 274b790..54b5fd8 100644 --- a/containers/trivy/trivy.config +++ b/containers/trivy/trivy.config @@ -18,6 +18,9 @@ CFG_TRIVY_APP_NAME=trivy # Not instanced by design. The updater resolves the scanner by the fixed container name trivy-service (see containers/trivy/scripts/trivy_scan.sh). A second copy would run but never be the one CVE scanning uses. CFG_TRIVY_MULTI_INSTANCE=false CFG_TRIVY_BACKUP=false +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move trivy ` — editing here records intent only. +CFG_TRIVY_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_TRIVY_BACKUP_STRATEGY=auto CFG_TRIVY_UPDATE_TYPE=auto CFG_TRIVY_COMPOSE_FILE=default diff --git a/containers/unbound/unbound.config b/containers/unbound/unbound.config index 6a3868c..9aafe6d 100755 --- a/containers/unbound/unbound.config +++ b/containers/unbound/unbound.config @@ -19,6 +19,8 @@ CFG_UNBOUND_APP_NAME=unbound # Not instance-safe. A DNS resolver must own its fixed port 5335; a second copy could not bind it. CFG_UNBOUND_MULTI_INSTANCE=false CFG_UNBOUND_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_UNBOUND_STORAGE=default # Storage Location - Fixed: other apps reference unbound by path **READONLY** CFG_UNBOUND_BACKUP_STRATEGY=auto CFG_UNBOUND_UPDATE_TYPE=auto CFG_UNBOUND_COMPOSE_FILE=default diff --git a/containers/vaultwarden/vaultwarden.config b/containers/vaultwarden/vaultwarden.config index ef4b30b..271ad2b 100755 --- a/containers/vaultwarden/vaultwarden.config +++ b/containers/vaultwarden/vaultwarden.config @@ -18,6 +18,9 @@ CFG_VAULTWARDEN_APP_NAME=vaultwarden # is instance-safe — see scripts/instance/instance_create.sh. CFG_VAULTWARDEN_MULTI_INSTANCE=true CFG_VAULTWARDEN_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move vaultwarden ` — editing here records intent only. +CFG_VAULTWARDEN_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_VAULTWARDEN_BACKUP_STRATEGY=auto CFG_VAULTWARDEN_UPDATE_TYPE=auto CFG_VAULTWARDEN_COMPOSE_FILE=default diff --git a/containers/vikunja/vikunja.config b/containers/vikunja/vikunja.config index b4799e1..ef6f7ab 100644 --- a/containers/vikunja/vikunja.config +++ b/containers/vikunja/vikunja.config @@ -18,6 +18,9 @@ CFG_VIKUNJA_APP_NAME=vikunja # is instance-safe — see scripts/instance/instance_create.sh. CFG_VIKUNJA_MULTI_INSTANCE=true CFG_VIKUNJA_BACKUP=true +# STORAGE = which storage location holds this app's data. Change it with +# `libreportal app move vikunja ` — editing here records intent only. +CFG_VIKUNJA_STORAGE=default # Storage Location - Which disk holds this app's data [default:Primary] CFG_VIKUNJA_BACKUP_STRATEGY=auto CFG_VIKUNJA_UPDATE_TYPE=auto CFG_VIKUNJA_COMPOSE_FILE=default diff --git a/containers/wireguard/wireguard.config b/containers/wireguard/wireguard.config index 8d95f11..1e43ca3 100755 --- a/containers/wireguard/wireguard.config +++ b/containers/wireguard/wireguard.config @@ -20,6 +20,8 @@ CFG_WIREGUARD_APP_NAME=wireguard CFG_WIREGUARD_MULTI_INSTANCE=false CFG_WIREGUARD_SUBNET= CFG_WIREGUARD_BACKUP=true +# STORAGE = which storage location holds this app's data. +CFG_WIREGUARD_STORAGE=default # Storage Location - Fixed: other apps reference wireguard by path **READONLY** CFG_WIREGUARD_BACKUP_STRATEGY=auto CFG_WIREGUARD_UPDATE_TYPE=auto CFG_WIREGUARD_COMPOSE_FILE=default diff --git a/scripts/cli/commands/app/cli_app_commands.sh b/scripts/cli/commands/app/cli_app_commands.sh index 2a58ef0..d5a9f26 100755 --- a/scripts/cli/commands/app/cli_app_commands.sh +++ b/scripts/cli/commands/app/cli_app_commands.sh @@ -158,6 +158,21 @@ cliHandleAppCommands() fi ;; + "move") + # `app move ` — relocate an app's data to another + # storage location. Long-running and it stops the app, so it goes + # through the task processor like backup/restore rather than + # blocking the CLI. + if [[ -z "$app_name" || -z "$initial_command4" ]]; then + isNotice "Usage: app move " + cliShowAppHelp + elif [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then + storageMoveApp "$app_name" "$initial_command4" "$initial_command5" + else + cliTaskRun "libreportal app move $app_name $initial_command4" "move" "$app_name" + fi + ;; + "backup") if [[ -z "$app_name" ]]; then isNotice "No app provided." diff --git a/scripts/cli/commands/app/cli_app_header.sh b/scripts/cli/commands/app/cli_app_header.sh index 6f91bfa..1a71c34 100755 --- a/scripts/cli/commands/app/cli_app_header.sh +++ b/scripts/cli/commands/app/cli_app_header.sh @@ -20,6 +20,10 @@ cliShowAppHelp() echo " catalog (marketplace). It then installs like any" echo " other app. Browse: libreportal artifact index" echo " libreportal app uninstall [name*] - Uninstall the specified app" + echo " libreportal app move [name*] [location*] - Move the app's data to another storage" + echo " location. Stops the app, snapshots it," + echo " copies, verifies, then removes the source." + echo " See: libreportal storage list" echo "" echo " libreportal app start [name*] - Start the specified app (Must be installed)" echo " libreportal app stop [name*] - Stop the specified app (Must be installed)" diff --git a/scripts/source/files/arrays/files_storage.sh b/scripts/source/files/arrays/files_storage.sh index 01c6458..541a751 100644 --- a/scripts/source/files/arrays/files_storage.sh +++ b/scripts/source/files/arrays/files_storage.sh @@ -4,9 +4,11 @@ # Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate storage_scripts=( + "storage/storage_app_config.sh" "storage/storage_checks.sh" "storage/storage_disks.sh" "storage/storage_locations.sh" + "storage/storage_move.sh" "storage/storage_scan.sh" ) diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index a88b122..de37146 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -1024,12 +1024,16 @@ declare -gA LP_FN_MAP=( [storageLocationConfig]="storage/storage_locations.sh" [storageLocationDir]="storage/storage_locations.sh" [storageLocationsDir]="storage/storage_locations.sh" + [storageMoveApp]="storage/storage_move.sh" + [_storageOptionList]="storage/storage_app_config.sh" [_storageProbeDir]="storage/storage_checks.sh" [_storageRefreshWebui]="storage/storage_locations.sh" [storageRemove]="storage/storage_locations.sh" [storageScan]="storage/storage_scan.sh" [storageScanCandidates]="storage/storage_scan.sh" [_storageSkipTarget]="storage/storage_scan.sh" + [storageSyncAllAppComments]="storage/storage_app_config.sh" + [storageSyncAppComment]="storage/storage_app_config.sh" [_storageWriteLocationConfig]="storage/storage_locations.sh" [switchMigrateBackupApps]="docker/type_switcher/swap_docker_type.sh" [switchMigrateRestoreApps]="docker/type_switcher/swap_docker_type.sh" @@ -2240,12 +2244,16 @@ declare -gA LP_FN_ROOT=( [storageLocationConfig]="scripts" [storageLocationDir]="scripts" [storageLocationsDir]="scripts" + [storageMoveApp]="scripts" + [_storageOptionList]="scripts" [_storageProbeDir]="scripts" [_storageRefreshWebui]="scripts" [storageRemove]="scripts" [storageScan]="scripts" [storageScanCandidates]="scripts" [_storageSkipTarget]="scripts" + [storageSyncAllAppComments]="scripts" + [storageSyncAppComment]="scripts" [_storageWriteLocationConfig]="scripts" [switchMigrateBackupApps]="scripts" [switchMigrateRestoreApps]="scripts" @@ -3493,12 +3501,16 @@ storageList() { unset -f storageList; __lpAutoload "${install_scripts_dir}storag storageLocationConfig() { unset -f storageLocationConfig; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationConfig "$@"; } storageLocationDir() { unset -f storageLocationDir; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationDir "$@"; } storageLocationsDir() { unset -f storageLocationsDir; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationsDir "$@"; } +storageMoveApp() { unset -f storageMoveApp; __lpAutoload "${install_scripts_dir}storage/storage_move.sh"; storageMoveApp "$@"; } +_storageOptionList() { unset -f _storageOptionList; __lpAutoload "${install_scripts_dir}storage/storage_app_config.sh"; _storageOptionList "$@"; } _storageProbeDir() { unset -f _storageProbeDir; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageProbeDir "$@"; } _storageRefreshWebui() { unset -f _storageRefreshWebui; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageRefreshWebui "$@"; } storageRemove() { unset -f storageRemove; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageRemove "$@"; } storageScan() { unset -f storageScan; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; storageScan "$@"; } storageScanCandidates() { unset -f storageScanCandidates; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; storageScanCandidates "$@"; } _storageSkipTarget() { unset -f _storageSkipTarget; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; _storageSkipTarget "$@"; } +storageSyncAllAppComments() { unset -f storageSyncAllAppComments; __lpAutoload "${install_scripts_dir}storage/storage_app_config.sh"; storageSyncAllAppComments "$@"; } +storageSyncAppComment() { unset -f storageSyncAppComment; __lpAutoload "${install_scripts_dir}storage/storage_app_config.sh"; storageSyncAppComment "$@"; } _storageWriteLocationConfig() { unset -f _storageWriteLocationConfig; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageWriteLocationConfig "$@"; } switchMigrateBackupApps() { unset -f switchMigrateBackupApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateBackupApps "$@"; } switchMigrateRestoreApps() { unset -f switchMigrateRestoreApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateRestoreApps "$@"; } diff --git a/scripts/storage/storage_app_config.sh b/scripts/storage/storage_app_config.sh new file mode 100644 index 0000000..9506383 --- /dev/null +++ b/scripts/storage/storage_app_config.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# Keeping CFG__STORAGE's comment truthful. +# +# The value is a location NAME, because names survive a migrate to a host whose +# disks are laid out differently and paths do not. But a name alone is useless to +# someone reading the config file at 2am with no tooling, so the RESOLVED PATH +# rides in the field's comment: +# +# CFG_NEXTCLOUD_STORAGE=bigdisk # Storage Location - Currently at /mnt/bigdisk/apps/nextcloud [default:Primary|bigdisk:Big disk] +# +# Two rules keep that from becoming a liability: +# +# 1. It is a BREADCRUMB, never a source of truth. Nothing reads it to decide +# anything — appDir resolves by discovery. A stale one is cosmetic. +# 2. It is written ONLY when it changes. The app .config is user-editable and +# lives in the container-owned tree, so an unconditional rewrite on every +# regen is both file churn and a runFileOp per app per pass. Compare first. + +# Rebuild the "[default:Primary|: