fix(webui): use the app's real title in the Tools and Services headers
Both _titleBlock implementations title-cased the slug themselves instead of
calling getAppDisplayName, so the Tools tab read "Run app-specific actions for
Rocketchat" and Services read "the docker compose services that make up
Speedtest".
getAppDisplayName already resolves a slug to the app's declared title through
window.apps. Using it fixes four apps beyond Rocket.Chat:
rocketchat Rocketchat -> Rocket.Chat
speedtest Speedtest -> LibreSpeed
ipinfo Ipinfo -> IPinfo
libreportal_catalog Libreportal Catalog -> LibrePortal Catalog
The slug casing is kept as the fallback for the window.apps-not-loaded-yet case,
which is what the helper does internally anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5087a88f65
commit
c3b7d6ae35
@ -86,7 +86,13 @@ class ServicesManager {
|
||||
}
|
||||
|
||||
_titleBlock(appName) {
|
||||
const display = (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
// Prefer the app's real title over a title-cased slug: getAppDisplayName
|
||||
// reads it from window.apps, so "rocketchat" reads "Rocket.Chat" rather
|
||||
// than "Rocketchat", and "ipinfo"/"speedtest" get "IPinfo"/"LibreSpeed".
|
||||
// The slug casing stays as the fallback for when window.apps has not
|
||||
// loaded yet.
|
||||
const display = (window.getAppDisplayName ? window.getAppDisplayName(appName) : '')
|
||||
|| (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
const adv = window.LpUi?.advanced?.get() ? 'checked' : '';
|
||||
// The toggle is the visible surface for the global "Advanced UI" mode
|
||||
// ([[window.LpUi.advanced]]). Flipping it here unhides the rich
|
||||
|
||||
@ -357,7 +357,13 @@ class ToolsManager {
|
||||
unload() { /* no timers/streams */ }
|
||||
|
||||
_titleBlock(appName) {
|
||||
const display = (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
// Prefer the app's real title over a title-cased slug: getAppDisplayName
|
||||
// reads it from window.apps, so "rocketchat" reads "Rocket.Chat" rather
|
||||
// than "Rocketchat", and "ipinfo"/"speedtest" get "IPinfo"/"LibreSpeed".
|
||||
// The slug casing stays as the fallback for when window.apps has not
|
||||
// loaded yet.
|
||||
const display = (window.getAppDisplayName ? window.getAppDisplayName(appName) : '')
|
||||
|| (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
return `
|
||||
<div class="tools-title">
|
||||
<h3>🧰 Tools</h3>
|
||||
|
||||
@ -1,5 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build the `unshare` prefix that lets a NON-ROOT restic recreate the container
|
||||
# uids a snapshot recorded.
|
||||
#
|
||||
# Why this is needed: backups run as the docker install user (runBackupOp — the
|
||||
# backup engine never gets root). A non-root restic cannot chown a restored file
|
||||
# to anyone else, so every file came back owned by that user. For LibrePortal's
|
||||
# own files that is correct; for the ones a CONTAINER owns it is fatal. Under
|
||||
# rootless, a container process running as uid N appears on the host as
|
||||
# subuid_start + N - 1 (prometheus' nobody -> 296605, postgres -> 231141), and an
|
||||
# app whose data dir is no longer owned by its own uid does not start:
|
||||
# prometheus dies on "open data/queries.active: permission denied", and postgres
|
||||
# refuses outright unless its data dir is 0700 and its own. Restores therefore
|
||||
# handed back apps that could not boot.
|
||||
#
|
||||
# The fix needs no new privilege. The docker install user already owns a subuid
|
||||
# range (that is what makes rootless work), so it may enter a user namespace in
|
||||
# which it is root and those subuids are mappable. Mapping them to THEMSELVES
|
||||
# means an id recorded in the snapshot is written back as the same host id.
|
||||
#
|
||||
# Files recorded as the docker install user's own uid are the one gap: that uid
|
||||
# is outside the subuid range and is already consumed by the inner-root mapping,
|
||||
# so restic's lchown for them fails with EINVAL. It is harmless — restic runs as
|
||||
# inner root, which IS that user on the host, so those files already land with
|
||||
# exactly the right owner. resticRestoreErrorsAreBenign below is what keeps that
|
||||
# from being reported as a failed restore.
|
||||
_resticUsernsPrefix()
|
||||
{
|
||||
local usr="${docker_install_user:-dockerinstall}"
|
||||
command -v unshare >/dev/null 2>&1 || return 0
|
||||
|
||||
local uline gline ustart ucount gstart gcount
|
||||
uline=$(grep "^${usr}:" /etc/subuid 2>/dev/null | head -1)
|
||||
gline=$(grep "^${usr}:" /etc/subgid 2>/dev/null | head -1)
|
||||
# No subuid range (rooted mode, or a hand-rolled account) — nothing to map,
|
||||
# so leave the call exactly as it was rather than guess.
|
||||
[[ -n "$uline" && -n "$gline" ]] || return 0
|
||||
|
||||
ustart="${uline#*:}"; ustart="${ustart%%:*}"; ucount="${uline##*:}"
|
||||
gstart="${gline#*:}"; gstart="${gstart%%:*}"; gcount="${gline##*:}"
|
||||
[[ "$ustart" =~ ^[0-9]+$ && "$ucount" =~ ^[0-9]+$ ]] || return 0
|
||||
[[ "$gstart" =~ ^[0-9]+$ && "$gcount" =~ ^[0-9]+$ ]] || return 0
|
||||
|
||||
printf '%s\n' unshare --map-root-user \
|
||||
"--map-users=${ustart}:${ustart}:${ucount}" \
|
||||
"--map-groups=${gstart}:${gstart}:${gcount}"
|
||||
}
|
||||
|
||||
# True when every error restic reported is the expected "cannot map the backup
|
||||
# user's own uid" one described above. Anything else — a missing pack, a full
|
||||
# disk, a permission problem on the target — must still fail the restore.
|
||||
resticRestoreErrorsAreBenign()
|
||||
{
|
||||
local out="$1"
|
||||
local bad
|
||||
# Every line restic prints for a failed ownership set, minus the benign form.
|
||||
bad=$(printf '%s\n' "$out" | grep -E "^ignoring error for " \
|
||||
| grep -vE "lchown .*: (invalid argument|operation not permitted)$")
|
||||
[[ -z "$bad" ]]
|
||||
}
|
||||
|
||||
resticRestoreSnapshot()
|
||||
{
|
||||
local idx="$1"
|
||||
@ -20,8 +80,25 @@ resticRestoreSnapshot()
|
||||
[[ -n "$include_path" ]] && args+=(--include "$include_path")
|
||||
|
||||
isNotice "Restoring ${snapshot_id:0:8} from $(resticLocationName "$idx") → $target_dir"
|
||||
runBackupOp restic "${args[@]}"
|
||||
local rc=$?
|
||||
|
||||
local ns_prefix=()
|
||||
mapfile -t ns_prefix < <(_resticUsernsPrefix)
|
||||
|
||||
# Output is captured (not streamed) so the benign-error check below can read
|
||||
# it; it is echoed straight back afterwards, so the operator sees the same
|
||||
# restic report as before.
|
||||
local out rc
|
||||
out=$(runBackupOp "${ns_prefix[@]}" restic "${args[@]}" 2>&1)
|
||||
rc=$?
|
||||
printf '%s\n' "$out"
|
||||
|
||||
# restic exits non-zero for the un-mappable-uid lchowns even though the files
|
||||
# themselves landed correctly. Only forgive that exact case.
|
||||
if [[ $rc -ne 0 && ${#ns_prefix[@]} -gt 0 ]] && resticRestoreErrorsAreBenign "$out"; then
|
||||
isNotice "Restore reported ownership warnings for LibrePortal's own files — expected, they are already owned correctly."
|
||||
rc=0
|
||||
fi
|
||||
|
||||
resticEnvUnset
|
||||
return $rc
|
||||
}
|
||||
|
||||
@ -78,7 +78,13 @@ restoreAppStart()
|
||||
isError "Restore failed — leaving app in stopped state"
|
||||
return 1
|
||||
fi
|
||||
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$containers_dir$stored_app_name"
|
||||
# NO blanket chown here. The snapshot's ownership is the thing being restored:
|
||||
# container-owned data (postgres, mongo, prometheus' store) must come back
|
||||
# under the container's uid or the app will not boot. resticRestoreSnapshot
|
||||
# now recreates those uids faithfully, and this line could only ever undo
|
||||
# that — it used to run as the docker install user, so against a correctly
|
||||
# restored tree it just fails file by file, and where it did "work" it was
|
||||
# cementing the ownership that broke the app.
|
||||
|
||||
((menu_number++))
|
||||
echo ""
|
||||
|
||||
@ -853,6 +853,7 @@ declare -gA LP_FN_MAP=(
|
||||
[resticLocationUri]="backup/engine/restic_env.sh"
|
||||
[resticNextFreeIndex]="backup/engine/restic_env.sh"
|
||||
[resticRestoreAppLatest]="backup/engine/restic_restore.sh"
|
||||
[resticRestoreErrorsAreBenign]="backup/engine/restic_restore.sh"
|
||||
[resticRestoreSnapshot]="backup/engine/restic_restore.sh"
|
||||
[resticRestoreSystemLatest]="backup/engine/restic_restore.sh"
|
||||
[resticRetentionFor]="backup/engine/restic_forget.sh"
|
||||
@ -860,6 +861,7 @@ declare -gA LP_FN_MAP=(
|
||||
[resticSnapshotListFiles]="backup/engine/restic_snapshots.sh"
|
||||
[resticSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
||||
[resticSystemSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
||||
[_resticUsernsPrefix]="backup/engine/restic_restore.sh"
|
||||
[restoreAppRunHook]="restore/restore_app_hooks.sh"
|
||||
[restoreAppStart]="restore/restore_app_start.sh"
|
||||
[restoreDbRehydratePreStart]="backup/db/backup_db.sh"
|
||||
@ -1976,6 +1978,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[resticLocationUri]="scripts"
|
||||
[resticNextFreeIndex]="scripts"
|
||||
[resticRestoreAppLatest]="scripts"
|
||||
[resticRestoreErrorsAreBenign]="scripts"
|
||||
[resticRestoreSnapshot]="scripts"
|
||||
[resticRestoreSystemLatest]="scripts"
|
||||
[resticRetentionFor]="scripts"
|
||||
@ -1983,6 +1986,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[resticSnapshotListFiles]="scripts"
|
||||
[resticSnapshotsJson]="scripts"
|
||||
[resticSystemSnapshotsJson]="scripts"
|
||||
[_resticUsernsPrefix]="scripts"
|
||||
[restoreAppRunHook]="scripts"
|
||||
[restoreAppStart]="scripts"
|
||||
[restoreDbRehydratePreStart]="scripts"
|
||||
@ -3134,6 +3138,7 @@ resticLocationType() { unset -f resticLocationType; __lpAutoload "${install_scri
|
||||
resticLocationUri() { unset -f resticLocationUri; __lpAutoload "${install_scripts_dir}backup/engine/restic_env.sh"; resticLocationUri "$@"; }
|
||||
resticNextFreeIndex() { unset -f resticNextFreeIndex; __lpAutoload "${install_scripts_dir}backup/engine/restic_env.sh"; resticNextFreeIndex "$@"; }
|
||||
resticRestoreAppLatest() { unset -f resticRestoreAppLatest; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; resticRestoreAppLatest "$@"; }
|
||||
resticRestoreErrorsAreBenign() { unset -f resticRestoreErrorsAreBenign; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; resticRestoreErrorsAreBenign "$@"; }
|
||||
resticRestoreSnapshot() { unset -f resticRestoreSnapshot; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; resticRestoreSnapshot "$@"; }
|
||||
resticRestoreSystemLatest() { unset -f resticRestoreSystemLatest; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; resticRestoreSystemLatest "$@"; }
|
||||
resticRetentionFor() { unset -f resticRetentionFor; __lpAutoload "${install_scripts_dir}backup/engine/restic_forget.sh"; resticRetentionFor "$@"; }
|
||||
@ -3141,6 +3146,7 @@ resticSnapshotLatestId() { unset -f resticSnapshotLatestId; __lpAutoload "${inst
|
||||
resticSnapshotListFiles() { unset -f resticSnapshotListFiles; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSnapshotListFiles "$@"; }
|
||||
resticSnapshotsJson() { unset -f resticSnapshotsJson; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSnapshotsJson "$@"; }
|
||||
resticSystemSnapshotsJson() { unset -f resticSystemSnapshotsJson; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSystemSnapshotsJson "$@"; }
|
||||
_resticUsernsPrefix() { unset -f _resticUsernsPrefix; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; _resticUsernsPrefix "$@"; }
|
||||
restoreAppRunHook() { unset -f restoreAppRunHook; __lpAutoload "${install_scripts_dir}restore/restore_app_hooks.sh"; restoreAppRunHook "$@"; }
|
||||
restoreAppStart() { unset -f restoreAppStart; __lpAutoload "${install_scripts_dir}restore/restore_app_start.sh"; restoreAppStart "$@"; }
|
||||
restoreDbRehydratePreStart() { unset -f restoreDbRehydratePreStart; __lpAutoload "${install_scripts_dir}backup/db/backup_db.sh"; restoreDbRehydratePreStart "$@"; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user