Establish the self-contained tools convention and prove it on a core app: - discovery now reads containers/<app>/tools/<app>.tools.json (the tools/ subfolder); tool functions live at containers/<app>/tools/*.sh, auto-sourced by the container scan (depth 3) — no scripts/app/ entry, no array regen. - adguard migrated: its 2 Tools-tab actions (reset_password, apply_dns_updater) moved to containers/adguard/tools/ + tools/adguard.tools.json, and dropped from the central webui_tools.sh heredoc. adguard_auth.sh stays in scripts/app/ — it's a logic helper, NOT a tool (the key distinction: only DECLARED tools move). Central + per-app styles coexist (pihole etc. still central), so the remaining apps can migrate one at a time with nothing breaking. Verified: heredoc valid sans adguard, per-app merge re-adds adguard's 2 tools, scripts array dropped the moved fns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
425 lines
21 KiB
Bash
425 lines
21 KiB
Bash
#!/bin/bash
|
||
|
||
# Single source of truth for the Tools tab. The frontend reads
|
||
# /data/apps/generated/apps-tools.json — this generator emits it.
|
||
#
|
||
# To add a tool:
|
||
# 1. Add an entry under that app's "tools" array below.
|
||
# 2. Drop a one-function file at scripts/app/containers/<app>/<app>_<tool_id>.sh
|
||
# with `app<App><PascalCase(toolId)>()` defined inside.
|
||
# 3. Re-run the WebUI updater (or call this function directly) to
|
||
# regenerate apps-tools.json.
|
||
#
|
||
# Tool entry schema (matches what tools-manager.js expects):
|
||
# {
|
||
# id: "<unique within the app>", # passed to dispatcher
|
||
# label: "Reset User Password", # button + modal heading
|
||
# description: "One-line summary", # shown on card + modal
|
||
# icon: "🔑", # any emoji
|
||
# destructive: false, # red button if true
|
||
# confirm: "Are you sure?", # forces a modal even
|
||
# # when fields is empty
|
||
# fields: [] # see field schema below
|
||
# }
|
||
#
|
||
# Field schema (each form input the modal collects):
|
||
# { name, label, type: text|password|number|select|checkbox|textarea,
|
||
# default, placeholder, required, options (for select), min, max }
|
||
webuiGenerateAppsToolsConfig() {
|
||
# The WebUI task service is a long-lived bash process that sources
|
||
# this file once at startup. When the auto-updater rewrites this
|
||
# script on disk (e.g. to add a new app's tools entry), the function
|
||
# in memory stays stale until the service restarts — producing
|
||
# apps-tools.json without the new entries. Re-source ourselves on
|
||
# every call so heredoc edits take effect immediately, then dispatch
|
||
# to the freshly-loaded version. The guard prevents infinite
|
||
# recursion.
|
||
# Skip the reload if the file is missing right now — happens briefly
|
||
# during update.sh quick (find -delete + cp). Fall through to the
|
||
# in-memory version (one cycle stale at worst).
|
||
if [[ -z "$_WEBUI_TOOLS_RELOADED" && -f "${BASH_SOURCE[0]}" ]]; then
|
||
local _WEBUI_TOOLS_RELOADED=1
|
||
source "${BASH_SOURCE[0]}"
|
||
webuiGenerateAppsToolsConfig "$@"
|
||
return $?
|
||
fi
|
||
|
||
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps-tools.json"
|
||
local tmp="$(mktemp)"
|
||
|
||
runFileOp mkdir -p "$(dirname "$output_file")"
|
||
|
||
# Heredoc carries the JSON literal verbatim — much easier to edit
|
||
# than escaping nested quotes through jq -n.
|
||
cat > "$tmp" <<'JSON'
|
||
{
|
||
"apps": {
|
||
"pihole": {
|
||
"tools": [
|
||
{
|
||
"id": "apply_dns_updater",
|
||
"label": "Apply DNS Updater",
|
||
"description": "Rewrite this server's /etc/resolv.conf to use Pi-hole as its DNS resolver right now. Same action that runs automatically when the global DNS Updater requirement is enabled.",
|
||
"icon": "🌐",
|
||
"fields": []
|
||
}
|
||
]
|
||
},
|
||
"dashy": {
|
||
"tools": [
|
||
{
|
||
"id": "manage_shortcuts",
|
||
"label": "Manage Shortcuts",
|
||
"description": "Pick which service URLs appear as shortcuts on the Dashy dashboard.",
|
||
"icon": "🧩",
|
||
"fields": [
|
||
{
|
||
"name": "selected",
|
||
"label": "URLs to show on the dashboard",
|
||
"type": "app_urls_multi",
|
||
"prefillFromCfgKey": "CFG_DASHY_SHORTCUTS",
|
||
"excludeApps": ["dashy"]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"bookstack": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password",
|
||
"category": "users",
|
||
"label": "Reset User Password",
|
||
"description": "Reset an existing Bookstack user's password. Leave the password field blank to generate a random one — it is shown in the task log.",
|
||
"icon": "🔑",
|
||
"fields": [
|
||
{ "name": "email", "label": "User email", "type": "text", "placeholder": "user@example.com", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account",
|
||
"category": "users",
|
||
"label": "Create User Account",
|
||
"description": "Create a new Bookstack user. Tick \"Make admin\" to grant full admin rights; otherwise the new user gets the default registration role. Leave the password blank to generate a random one.",
|
||
"icon": "👤",
|
||
"fields": [
|
||
{ "name": "email", "label": "Email", "type": "text", "placeholder": "user@example.com", "required": true },
|
||
{ "name": "name", "label": "Display name", "type": "text", "required": true },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{ "id": "list_users", "category": "users", "label": "List Users", "description": "Show every Bookstack user with their roles.", "icon": "📋", "fields": [] },
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user account.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone.",
|
||
"fields": [
|
||
{ "name": "email", "label": "User email", "type": "text", "required": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "set_admin", "category": "users", "label": "Set Admin Status",
|
||
"description": "Promote a user to admin or demote them to a normal user.", "icon": "👑",
|
||
"fields": [
|
||
{ "name": "email", "label": "User email", "type": "text", "required": true },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"focalboard": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password",
|
||
"category": "users",
|
||
"label": "Reset User Password",
|
||
"description": "Reset an existing Focalboard user's password.",
|
||
"icon": "🔑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account",
|
||
"category": "users",
|
||
"label": "Create User Account",
|
||
"description": "Create a new Focalboard user. Leave password blank to generate one.",
|
||
"icon": "👤",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "email", "label": "Email", "type": "text", "required": true },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password" },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{
|
||
"id": "list_users",
|
||
"category": "users",
|
||
"label": "List Users",
|
||
"description": "Show every Focalboard user account.",
|
||
"icon": "📋",
|
||
"fields": []
|
||
},
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user account.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone.",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"mattermost": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password", "category": "users", "label": "Reset User Password",
|
||
"description": "Reset a Mattermost user's password via mmctl.", "icon": "🔑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username or Email", "type": "text", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account", "category": "users", "label": "Create User Account",
|
||
"description": "Create a new Mattermost user via mmctl.", "icon": "👤",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "email", "label": "Email", "type": "text", "required": true },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password" },
|
||
{ "name": "admin", "label": "Make system admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{ "id": "list_users", "category": "users", "label": "List Users", "description": "Show every Mattermost user account.", "icon": "📋", "fields": [] },
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user account.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone.",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username/Email", "type": "text", "required": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "set_admin", "category": "users", "label": "Set Admin Status",
|
||
"description": "Promote a user to admin or demote them to a normal user.", "icon": "👑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username/Email", "type": "text", "required": true },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"invidious": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password", "category": "users", "label": "Reset User Password",
|
||
"description": "Reset an Invidious user's password (Postgres bcrypt update).", "icon": "🔑",
|
||
"fields": [
|
||
{ "name": "email", "label": "Email", "type": "text", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account", "category": "users", "label": "Create User Account",
|
||
"description": "Create a new Invidious user (Invidious uses email as username).", "icon": "👤",
|
||
"fields": [
|
||
{ "name": "email", "label": "Email", "type": "text", "required": true },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password" },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{ "id": "list_users", "category": "users", "label": "List Users", "description": "Show every Invidious user account.", "icon": "📋", "fields": [] },
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user account.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone.",
|
||
"fields": [
|
||
{ "name": "email", "label": "Email", "type": "text", "required": true }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"gitea": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password",
|
||
"category": "users",
|
||
"label": "Reset User Password",
|
||
"description": "Reset a Gitea user's password using the built-in `gitea admin user change-password` CLI. Leave the password blank to generate a random one.",
|
||
"icon": "🔑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account",
|
||
"category": "users",
|
||
"label": "Create User Account",
|
||
"description": "Create a new Gitea user via the built-in `gitea admin user create` CLI. Tick \"Make admin\" for full admin rights. Leave the password blank to generate a random one. The account starts ready to log in (no forced password change).",
|
||
"icon": "👤",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "email", "label": "Email", "type": "text", "placeholder": "user@example.com", "required": true },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{ "id": "list_users", "category": "users", "label": "List Users", "description": "Show every Gitea user account.", "icon": "📋", "fields": [] },
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user account.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone.",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "set_admin", "category": "users", "label": "Set Admin Status",
|
||
"description": "Promote a user to admin or demote them to a normal user.", "icon": "👑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"gluetun": {
|
||
"tools": [
|
||
{
|
||
"id": "refresh_providers",
|
||
"label": "Refresh VPN Providers",
|
||
"description": "Re-scan VPN providers and country lists, regenerating the snapshot used by the country picker.",
|
||
"icon": "🔄",
|
||
"fields": []
|
||
}
|
||
]
|
||
},
|
||
"traefik": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password",
|
||
"label": "Reset Dashboard Login",
|
||
"description": "Reset the Traefik dashboard credentials (CFG_TRAEFIK_USER / CFG_TRAEFIK_PASS) and regenerate the htpasswd entry in protectionauth.yml. Leave fields blank to keep the current username and/or generate a new random password.",
|
||
"icon": "🔑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username (leave blank to keep current)", "type": "text", "placeholder": "admin" },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"nextcloud": {
|
||
"tools": [
|
||
{
|
||
"id": "reset_password",
|
||
"category": "users",
|
||
"label": "Reset User Password",
|
||
"description": "Reset an existing Nextcloud user's password. Leave the password field blank to generate a random one — it is shown in the task log.",
|
||
"icon": "🔑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "placeholder": "alice", "required": true },
|
||
{ "name": "password", "label": "New password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" }
|
||
]
|
||
},
|
||
{
|
||
"id": "create_account",
|
||
"category": "users",
|
||
"label": "Create User Account",
|
||
"description": "Create a new Nextcloud user. Tick \"Make admin\" to add them to the admin group. Leave the password blank to generate a random one.",
|
||
"icon": "👤",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "placeholder": "alice", "required": true },
|
||
{ "name": "display_name", "label": "Display name", "type": "text", "placeholder": "Alice Smith" },
|
||
{ "name": "password", "label": "Password (leave blank to generate)", "type": "password", "placeholder": "Leave blank for random" },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{ "id": "list_users", "category": "users", "label": "List Users", "description": "Show every Nextcloud user with their display name and admin flag.", "icon": "📋", "fields": [] },
|
||
{
|
||
"id": "delete_user", "category": "users", "label": "Delete User Account",
|
||
"description": "Permanently delete a user and all their files.", "icon": "🗑",
|
||
"destructive": true, "confirm": "This cannot be undone. The user's files will be removed.",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "set_admin", "category": "users", "label": "Set Admin Status",
|
||
"description": "Add a user to (or remove from) the admin group.", "icon": "👑",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username", "type": "text", "required": true },
|
||
{ "name": "admin", "label": "Make admin", "type": "checkbox", "default": false }
|
||
]
|
||
},
|
||
{
|
||
"id": "toggle_maintenance", "category": "maintenance", "label": "Toggle Maintenance Mode",
|
||
"description": "Lock all users out and show a maintenance notice — required before running upgrades or repairs from the CLI.", "icon": "🚧",
|
||
"fields": [
|
||
{ "name": "enable", "label": "Enable maintenance mode", "type": "checkbox", "default": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "rescan_files", "category": "maintenance", "label": "Rescan Files",
|
||
"description": "Re-index Nextcloud's file metadata. Run this after files were added or removed on disk outside Nextcloud (rsync, restore, manual copy). Leave the username blank to scan every user.", "icon": "🔄",
|
||
"fields": [
|
||
{ "name": "username", "label": "Username (blank = all users)", "type": "text", "placeholder": "blank for all" }
|
||
]
|
||
},
|
||
{
|
||
"id": "add_trusted_domain", "category": "system", "label": "Add Trusted Domain",
|
||
"description": "Append a hostname to Nextcloud's trusted_domains list so requests to that host are accepted.", "icon": "🌐",
|
||
"fields": [
|
||
{ "name": "domain", "label": "Domain", "type": "text", "placeholder": "cloud.example.org", "required": true }
|
||
]
|
||
},
|
||
{
|
||
"id": "system_status", "category": "system", "label": "System Status",
|
||
"description": "Show Nextcloud's version, install state, and maintenance flag.", "icon": "ℹ️",
|
||
"fields": []
|
||
},
|
||
{
|
||
"id": "tail_logs", "category": "system", "label": "Tail Logs",
|
||
"description": "Show the most recent lines of nextcloud.log.", "icon": "📜",
|
||
"fields": [
|
||
{ "name": "lines", "label": "Lines", "type": "number", "default": 100, "min": 10, "max": 1000 }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
JSON
|
||
|
||
# Merge per-app tool declarations so a DROPPED-IN app (e.g. from LibrePortal-Infra)
|
||
# registers its own Tools tab actions without editing this file. Each app may ship
|
||
# containers/<app>/<app>.tools.json = { "tools": [ … ] } (same schema as above);
|
||
# it sets .apps[<app>]. Core apps declared in the heredoc need no such file.
|
||
if command -v jq >/dev/null 2>&1; then
|
||
local _tj _app
|
||
for _tj in "${install_containers_dir}"*/tools/*.tools.json; do
|
||
[[ -f "$_tj" ]] || continue
|
||
_app="$(basename "$(dirname "$(dirname "$_tj")")")" # …/<app>/tools/<x>.tools.json
|
||
if jq -e . "$_tj" >/dev/null 2>&1; then
|
||
jq --arg app "$_app" --slurpfile t "$_tj" '.apps[$app] = $t[0]' "$tmp" > "$tmp.m" && mv "$tmp.m" "$tmp"
|
||
else
|
||
isNotice "Skipping invalid tools file: $_tj"
|
||
fi
|
||
done
|
||
fi
|
||
|
||
if command -v jq >/dev/null 2>&1; then
|
||
if ! jq . "$tmp" >/dev/null 2>&1; then
|
||
isNotice "Generated apps-tools.json failed JSON validation; keeping existing file."
|
||
rm -f "$tmp"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
runFileWrite "$output_file" < "$tmp"; rm -f "$tmp"
|
||
isSuccessful "Generated apps-tools.json ($(jq '[.apps[].tools | length] | add' "$output_file" 2>/dev/null || echo "?") tool(s) across $(jq '.apps | length' "$output_file" 2>/dev/null || echo "?") app(s))."
|
||
}
|