diff --git a/containers/libreportal/backend/routes/docker-info-routes.js b/containers/libreportal/backend/routes/docker-info-routes.js index 69a7935..b378acc 100644 --- a/containers/libreportal/backend/routes/docker-info-routes.js +++ b/containers/libreportal/backend/routes/docker-info-routes.js @@ -368,12 +368,12 @@ router.get('/storage', async (req, res) => { }, { count: 0, size: 0, reclaimable: 0 } ); - // Largest images for "what's eating disk" surface; cap to 10 to - // keep the payload tight. - const topImages = (df.Images || []) + // Every image, largest first — the Storage page lists them all so the + // user can remove specific ones (deletion runs through the task/CLI, + // not from here). Dangling images are included on purpose. + const images = (df.Images || []) .slice() .sort((a, b) => (b.Size || 0) - (a.Size || 0)) - .slice(0, 10) .map(im => ({ id: im.Id, repo_tags: im.RepoTags || [], @@ -389,7 +389,7 @@ router.get('/storage', async (req, res) => { total, reclaimable, images: sumImages, build_cache: sumBuild, - top_images: topImages, + image_list: images, updated: new Date().toISOString(), }); } catch (err) { diff --git a/containers/libreportal/frontend/css/admin.css b/containers/libreportal/frontend/css/admin.css index acac836..120e358 100644 --- a/containers/libreportal/frontend/css/admin.css +++ b/containers/libreportal/frontend/css/admin.css @@ -61,6 +61,22 @@ } .admin-integrity .admin-status-dot { width: 8px; height: 8px; } +/* Storage → Images list: reuses the Tasks list look (.task-item etc., loaded + globally), with a small toolbar + image-specific status pills. */ +.sys-images-toolbar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 14px; + margin: 10px 0 6px; +} +.sys-images-list { margin-top: 4px; } +.sys-image-item .task-header { cursor: default; } +.sys-img-icon { color: rgba(var(--text-rgb), 0.6); flex-shrink: 0; } +.sys-img-pill.is-inuse { background: rgba(var(--accent-rgb), 0.16); color: var(--accent); } +.sys-img-pill.is-unused { background: rgba(var(--text-rgb), 0.10); color: rgba(var(--text-rgb), 0.6); } +.sys-img-pill.is-dangling { background: rgba(251, 189, 35, 0.16); color: #fbbd23; } + .admin-card-body { display: flex; flex-direction: column; diff --git a/containers/libreportal/frontend/js/components/admin/system-storage-page.js b/containers/libreportal/frontend/js/components/admin/system-storage-page.js index f3a4e88..8ea7461 100644 --- a/containers/libreportal/frontend/js/components/admin/system-storage-page.js +++ b/containers/libreportal/frontend/js/components/admin/system-storage-page.js @@ -7,12 +7,15 @@ // generator). The headline view for LibrePortal, whose data lives in bind // mounts, not named volumes. // - Docker engine `docker system df` — headline total + reclaimable, a -// donut (images / build cache), per-category cards, and the top images by -// size. The daemon's own overhead, separate from the per-app data. +// donut (images / build cache), per-category cards, and a Tasks-style list +// of every image (removable individually or in bulk). The daemon's own +// overhead, separate from the per-app data. // // One backend call (GET /api/system/storage), cached server-side for 5s. -// A "Reclaim space" button runs the safe prune (build cache + dangling -// images, never app data) via the system_reclaim task, then re-reads usage. +// Mutations go through the task system, never a direct API: "Reclaim space" +// runs the safe prune via the system_reclaim task; image removal runs +// `libreportal system image rm` via the system_image_rm task. Both re-read +// usage when the task lands. class SystemStoragePage { constructor(rootId = 'config-section') { @@ -20,7 +23,10 @@ class SystemStoragePage { this.data = null; this._timer = null; this._expanded = new Set(); // app names whose folder breakdown is open + this._selectedImageIds = new Set(); // images ticked for bulk delete + this._deleting = false; // a remove-images task is in flight this._onClick = this._onClick.bind(this); + this._onChange = this._onChange.bind(this); } root() { return document.getElementById(this.rootId); } @@ -30,13 +36,16 @@ class SystemStoragePage { await this._load(); this._render(); const r = this.root(); - if (r) r.addEventListener('click', this._onClick); - // Refresh every 30s while mounted. + if (r) { r.addEventListener('click', this._onClick); r.addEventListener('change', this._onChange); } + // Refresh every 30s while mounted. Skipped while a delete task is in + // flight so it doesn't fight the post-completion refresh or re-enable + // buttons mid-operation. this._timer = setInterval(() => { if (!document.querySelector('.sys-storage-page')) { clearInterval(this._timer); this._timer = null; return; } + if (this._deleting) return; this._load().then(() => this._render()); }, 30000); } @@ -44,7 +53,7 @@ class SystemStoragePage { dispose() { if (this._timer) { clearInterval(this._timer); this._timer = null; } const r = this.root(); - if (r) r.removeEventListener('click', this._onClick); + if (r) { r.removeEventListener('click', this._onClick); r.removeEventListener('change', this._onChange); } } _onClick(e) { @@ -55,6 +64,18 @@ class SystemStoragePage { } const recl = e.target.closest('[data-storage-reclaim]'); if (recl) { this._reclaim(recl); return; } + // Remove a single image. + const del = e.target.closest('[data-image-delete]'); + if (del) { this._deleteImages([del.getAttribute('data-image-delete')]); return; } + // Clear All / Delete Selected. + const clr = e.target.closest('[data-images-clear]'); + if (clr) { + const ids = this._selectedImageIds.size + ? [...this._selectedImageIds] + : ((this.data && this.data.image_list) || []).map(im => im.id); + this._deleteImages(ids); + return; + } // Expand/collapse an app's per-folder breakdown. Toggle the DOM directly // (no re-render) and remember the state so the 30s refresh re-applies it. const tog = e.target.closest('[data-app-toggle]'); @@ -69,6 +90,13 @@ class SystemStoragePage { } } + _onChange(e) { + const box = e.target.closest('[data-image-select]'); + if (box) { this.toggleImageSelection(box.getAttribute('data-image-select'), box.checked); return; } + const all = e.target.closest('[data-images-select-all]'); + if (all) { this.toggleSelectAllImages(all.checked); return; } + } + // Confirm, then run the safe reclaim task and re-read usage as it lands. // The button lives in the header (not the body), so it survives the // _render() refreshes below — re-enable it explicitly when done. @@ -283,31 +311,265 @@ class SystemStoragePage { `; }).join(''); - const topImages = Array.isArray(d.top_images) ? d.top_images : []; - const imagesTable = topImages.length ? ` -

Largest images

top ${topImages.length} by size
-
- - - - ${topImages.map(im => ` - - - - - - - `).join('')} - -
TagSizeSharedContainersCreated
${fmt.escape((im.repo_tags && im.repo_tags[0]) || im.id?.slice(0, 19) || '—')}${fmt.bytes(im.size)}${fmt.bytes(im.shared_size || 0)}${im.containers}${im.containers === 0 ? ' unused' : ''}${im.created ? fmt.timeAgo(im.created) : '—'}
-
` : ''; + const images = Array.isArray(d.image_list) ? d.image_list : []; dockerSection = `

Docker engine

${fmt.bytes(total)} · ${fmt.bytes(recl)} reclaimable — the daemon's own usage, separate from your app data
${cards}
- ${imagesTable}`; + ${this._renderImages(images)}`; } body.innerHTML = `${headline}${appsSection}${dockerSection}`; + // Re-apply selection state (button label + master checkbox) after the + // innerHTML rebuild so it survives the 30s refresh. + this._updateImageSelectionUI(); + } + + // ---- Images list (Tasks-style: select, bulk-delete) --------------------- + + // Friendly image label: first repo tag, else a short id. tags mean a + // dangling layer — show the short id instead. + _imageName(im) { + const tag = (im.repo_tags || []).find(t => t && !t.includes('')); + if (tag) return tag; + const id = (im.id || '').replace(/^sha256:/, ''); + return id ? id.slice(0, 12) : '—'; + } + + // {cls, label} for the status pill: in use / unused / dangling. Never red — + // an unused or dangling image isn't "wrong", just reclaimable. + _imagePill(im) { + const dangling = !(im.repo_tags || []).some(t => t && !t.includes('')); + if (im.containers > 0) return { cls: 'is-inuse', label: `in use · ${im.containers}` }; + if (dangling) return { cls: 'is-dangling', label: 'dangling' }; + return { cls: 'is-unused', label: 'unused' }; + } + + _renderImages(images) { + const fmt = window.SystemFmt; + const trash = ``; + const imgIcon = ``; + const totalSize = images.reduce((a, im) => a + (im.size || 0), 0); + + const head = ` +
+

Images

+ ${images.length} image${images.length === 1 ? '' : 's'} · ${fmt.bytes(totalSize)} +
`; + + if (!images.length) { + return `${head}
No images on disk.
`; + } + + const toolbar = ` +
+ + +
`; + + const rows = images.map(im => { + const id = fmt.escape(im.id || ''); + const pill = this._imagePill(im); + const selected = this._selectedImageIds.has(im.id); + return ` +
+
+
+ ${imgIcon} + ${fmt.escape(this._imageName(im))} + ${fmt.escape(pill.label)} + ${fmt.bytes(im.size || 0)} + ${im.shared_size ? `${fmt.bytes(im.shared_size)} shared` : ''} + ${im.created ? `${fmt.timeAgo(im.created)}` : ''} +
+
+ + +
+
+
`; + }).join(''); + + return `${head}${toolbar}
${rows}
`; + } + + // ---- selection (mirrors TasksManager) ----------------------------------- + toggleImageSelection(id, checked) { + if (checked) this._selectedImageIds.add(id); + else this._selectedImageIds.delete(id); + this._updateImageSelectionUI(); + } + + toggleSelectAllImages(checked) { + const boxes = this.root()?.querySelectorAll('#sys-images-list [data-image-select]') || []; + if (checked) boxes.forEach(cb => { this._selectedImageIds.add(cb.getAttribute('data-image-select')); cb.checked = true; }); + else { this._selectedImageIds.clear(); boxes.forEach(cb => { cb.checked = false; }); } + this._updateImageSelectionUI(); + } + + // Button label morph (Clear All ↔ Delete Selected (N)) + master tri-state. + _updateImageSelectionUI() { + const r = this.root(); + if (!r) return; + // Drop ids no longer present (e.g. after a refresh removed them). + const present = new Set(((this.data && this.data.image_list) || []).map(im => im.id)); + this._selectedImageIds = new Set([...this._selectedImageIds].filter(id => present.has(id))); + + const n = this._selectedImageIds.size; + const label = r.querySelector('#sys-images-clear .clear-btn-label'); + if (label) label.textContent = n > 0 ? `Delete Selected (${n})` : 'Clear All'; + + const master = r.querySelector('#sys-images-select-all'); + const visible = r.querySelectorAll('#sys-images-list [data-image-select]'); + if (master) { + if (n === 0 || visible.length === 0) { master.checked = false; master.indeterminate = false; } + else if (n >= visible.length) { master.checked = true; master.indeterminate = false; } + else { master.checked = false; master.indeterminate = true; } + } + } + + // ---- delete flow (routes through the system_image_rm task) -------------- + async _deleteImages(ids) { + const list = (this.data && this.data.image_list) || []; + const byId = new Map(list.map(im => [im.id, im])); + const targetsAll = ids.map(id => byId.get(id)).filter(Boolean); + if (!targetsAll.length) return; + + const inUse = targetsAll.filter(im => im.containers > 0); + const decision = await this._showImageDeleteModal(targetsAll, inUse); + if (!decision || !decision.confirmed) return; + + // Skip in-use images unless the user forced it. + const targets = decision.force ? targetsAll : targetsAll.filter(im => im.containers === 0); + const targetIds = targets.map(im => im.id); + if (!targetIds.length) { + window.notificationSystem?.show?.('Nothing removed — selected images are in use. Enable force to remove them.', 'info'); + return; + } + + if (!window.tasksManager?.router?.routeAction) { + window.notificationSystem?.show?.('Task system not ready', 'error'); + return; + } + + this._deleting = true; + const clearBtn = this.root()?.querySelector('#sys-images-clear'); + if (clearBtn) clearBtn.disabled = true; + this.root()?.querySelectorAll('[data-image-delete]').forEach(b => { b.disabled = true; }); + + const finish = () => { + this._deleting = false; + this._selectedImageIds.clear(); + this._load().then(() => this._render()); + }; + + try { + const task = await window.tasksManager.router.routeAction('system_image_rm', { ids: targetIds, force: decision.force }); + const taskId = task && task.id; + if (taskId) { + const onDone = (ev) => { + if (ev?.detail?.taskId !== taskId) return; + window.removeEventListener('taskCompleted', onDone); + clearTimeout(fallback); + const failed = ev?.detail?.task?.status === 'failed'; + window.notificationSystem?.show?.( + failed ? 'Some images could not be removed (see Tasks)' : 'Images removed', + failed ? 'warning' : 'success'); + finish(); + }; + // Safety net if the completion event is missed. + const fallback = setTimeout(() => { window.removeEventListener('taskCompleted', onDone); finish(); }, 12000); + window.addEventListener('taskCompleted', onDone); + } else { + setTimeout(finish, 4000); + } + } catch (err) { + this._deleting = false; + if (clearBtn) clearBtn.disabled = false; + window.notificationSystem?.show?.(`Could not start image removal: ${err.message || err}`, 'error'); + } + } + + // Confirmation modal — same eo shell as the Tasks Clear-All modal. Shows a + // force toggle only when in-use images are in the target set. Resolves + // {confirmed, force}. + _showImageDeleteModal(targets, inUse) { + return new Promise((resolve) => { + if (!window.openEoModal) { resolve({ confirmed: confirm(`Remove ${targets.length} image(s)?`), force: false }); return; } + const fmt = window.SystemFmt; + const total = targets.length; + const inUseN = inUse.length; + const freeN = total - inUseN; + const totalSize = targets.reduce((a, im) => a + (im.size || 0), 0); + + const badges = window.eoBadgeRow ? window.eoBadgeRow([ + { label: `Images: ${total}`, variant: 'info' }, + { label: `Size: ${fmt.bytes(totalSize)}`, variant: 'info' }, + ...(inUseN > 0 ? [{ label: `In use: ${inUseN}`, variant: 'warning' }] : []), + ]) : ''; + + const body = ` +
+
+ +
+
+

This cannot be undone

+

Removed images must be pulled or rebuilt to use again.

+
+
+ ${badges} + ${inUseN > 0 ? ` + ` : ''}`; + + let decided = false; + const finish = (val, modal) => { if (decided) return; decided = true; if (modal) modal.close(); resolve(val); }; + const m = window.openEoModal({ + id: 'remove-images-modal', + size: 'sm', + eyebrow: 'Remove Images', + title: total === 1 ? 'Remove this image?' : `Remove ${total} images?`, + desc: 'Confirm to remove the selected image(s) from the Docker engine.', + body, + actions: [ + { label: 'Remove', variant: 'danger', onClick: (modal) => { + const cb = modal.bodyEl.querySelector('#img-force'); + finish({ confirmed: true, force: !!(cb && cb.checked) }, modal); + } }, + { label: 'Cancel', variant: 'secondary', onClick: (modal) => finish({ confirmed: false }, modal) }, + ], + onClose: () => finish({ confirmed: false }, null), + }); + + // Live-update the danger button label as the force toggle flips. + const cb = m.bodyEl.querySelector('#img-force'); + const btn = m.contentEl.querySelector('.btn-danger'); + const sync = () => { + if (!btn) return; + const force = !!(cb && cb.checked); + if (inUseN > 0 && !force) btn.textContent = freeN > 0 ? `Remove ${freeN} (skip ${inUseN} in use)` : `Nothing to remove`; + else btn.textContent = total === 1 ? 'Remove Image' : `Remove ${total} Images`; + }; + if (cb) cb.addEventListener('change', sync); + sync(); + }); } } diff --git a/containers/libreportal/frontend/js/components/task/task-actions.js b/containers/libreportal/frontend/js/components/task/task-actions.js index c94e513..e0e67a1 100755 --- a/containers/libreportal/frontend/js/components/task/task-actions.js +++ b/containers/libreportal/frontend/js/components/task/task-actions.js @@ -242,6 +242,24 @@ async configUpdate(changes) { } } + // Remove Docker images by id. ids: array of image refs (the Storage page + // sends full sha256:… ids). Routes through the CLI's `system image rm` as a + // task — no direct API. Returns the created task so the caller can await its + // completion. force=true passes --force (in-use images). + async removeImages(ids, force = false) { + try { + const list = Array.isArray(ids) ? ids.filter(Boolean) : []; + if (!list.length) throw new Error('No images selected'); + const flag = force ? ' --force' : ''; + // Ids joined with commas so they stay a single CLI token. + const command = `libreportal system image rm${flag} ${list.join(',')}`; + const label = list.length === 1 ? 'Remove Image' : `Remove ${list.length} Images`; + return await this.executeTask('system_image_rm', 'system', command, label); + } catch (error) { + throw new Error(`Failed to remove images: ${error.message}`); + } + } + /** * Create a task object */ diff --git a/containers/libreportal/frontend/js/components/task/task-router.js b/containers/libreportal/frontend/js/components/task/task-router.js index e0be77b..275d741 100755 --- a/containers/libreportal/frontend/js/components/task/task-router.js +++ b/containers/libreportal/frontend/js/components/task/task-router.js @@ -63,6 +63,9 @@ class TaskRouter { case 'system_reclaim': return await this.actions.systemReclaim(); + case 'system_image_rm': + return await this.actions.removeImages(params.ids, params.force); + case 'tool': return await this.actions.runTool(params.appName, params.toolName, params.toolArgs, params.toolLabel); diff --git a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js index 71457ea..b640b26 100755 --- a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js +++ b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js @@ -74,7 +74,8 @@ class TasksManager { 'backup': 'Backup', 'restore': 'Restore', 'delete': 'Delete Backup', 'delete_all': 'Delete All Backups', 'config_update': 'Update Config', 'update_config': 'Update Config', - 'system_update': 'Update System', + 'system_update': 'Update System', 'system_reclaim': 'Reclaim Space', + 'system_image_rm': 'Remove Images', 'setup-config': 'Apply Configuration', 'setup-finalize': 'Finalize Setup' }; @@ -988,6 +989,10 @@ class TasksManager { // -- Regen ------------------------------------------------------------- { match: /^libreportal regen\b/, title: 'LibrePortal - Regenerate WebUI Data' }, + // -- System maintenance ------------------------------------------------ + { match: /^libreportal system reclaim\b/, title: 'LibrePortal - Reclaim Space' }, + { match: /^libreportal system image rm\b/, title: 'LibrePortal - Remove Images' }, + // -- Backup: per-app (these capture the app slug) ---------------------- { match: /^libreportal backup app create (\w+)/, title: (m) => `${displayName(m[1])} - Create Backup` }, { match: /^libreportal backup app schedule (\w+)/, title: (m) => `${displayName(m[1])} - Scheduled Backup` }, @@ -1081,6 +1086,7 @@ class TasksManager { 'restore': { icon: '📦', class: 'restore' }, 'delete': { icon: '🗑️', class: 'delete' }, 'delete_all': { icon: '🗑️', class: 'delete' }, + 'system_image_rm': { icon: '🗑️', class: 'delete' }, 'setup-config': { icon: '🛠️', class: 'setup' }, 'setup-finalize': { icon: '🎉', class: 'setup' }, 'custom': { icon: '⚙️', class: 'custom' } diff --git a/scripts/cli/commands/system/cli_system_commands.sh b/scripts/cli/commands/system/cli_system_commands.sh index 2d99afa..5ff8917 100755 --- a/scripts/cli/commands/system/cli_system_commands.sh +++ b/scripts/cli/commands/system/cli_system_commands.sh @@ -19,6 +19,51 @@ reclaimDockerSpace() isSuccessful "Done" } +# Remove specific Docker images by id/ref. Args: +# $1 force flag — "-f" to force-remove (e.g. in-use images), else empty +# $2 comma-separated list of image refs (the WebUI sends full sha256:… ids) +# The WebUI Storage page calls this through the task system (see the +# system_image_rm action) — never a direct API. Each ref is validated before it +# reaches docker; removal continues past per-image failures and reports a tally. +removeDockerImages() +{ + local force_flag="$1" ids_csv="$2" + + isHeader "Removing Images" + if [[ -z "$ids_csv" ]]; then + isError "No images specified." + return 1 + fi + + local removed=0 failed=0 id + local IFS=',' + local -a ids=($ids_csv) + unset IFS + + for id in "${ids[@]}"; do + id="${id//[[:space:]]/}" + [[ -n "$id" ]] || continue + # Accept only a sha256 digest or a conservative repo[:tag] ref — no shell + # metacharacters can reach docker even though this arrives via the task + # command string. + if [[ ! "$id" =~ ^sha256:[a-f0-9]{12,64}$ && ! "$id" =~ ^[A-Za-z0-9][A-Za-z0-9._/:@-]*$ ]]; then + isError "Skipping invalid image ref: $id" + failed=$((failed + 1)); continue + fi + # $force_flag is intentionally unquoted: it's either empty or "-f". + if runFileOp docker image rm $force_flag "$id" >/dev/null 2>&1; then + isSuccessful "Removed $id" + removed=$((removed + 1)) + else + isError "Could not remove $id (in use, or has dependent child images)" + failed=$((failed + 1)) + fi + done + + isNotice "Done — removed ${removed}, failed/skipped ${failed}." + [[ "$failed" -eq 0 ]] +} + cliHandleSystemCommands() { local action="$initial_command2" @@ -40,6 +85,25 @@ cliHandleSystemCommands() reclaimDockerSpace ;; + "image") + # libreportal system image rm [--force] + case "$initial_command3" in + "rm"|"remove") + local img_force="" img_ids="" + if [[ "$initial_command4" == "--force" || "$initial_command4" == "-f" ]]; then + img_force="-f"; img_ids="$initial_command5" + else + img_ids="$initial_command4" + fi + removeDockerImages "$img_force" "$img_ids" + ;; + *) + isNotice "Invalid image command: $initial_command3" + cliShowSystemHelp + ;; + esac + ;; + *) isNotice "Invalid system command: $action" cliShowSystemHelp diff --git a/scripts/cli/commands/system/cli_system_header.sh b/scripts/cli/commands/system/cli_system_header.sh index 84efa6e..7d25f6f 100755 --- a/scripts/cli/commands/system/cli_system_header.sh +++ b/scripts/cli/commands/system/cli_system_header.sh @@ -12,5 +12,6 @@ cliShowSystemHelp() echo " libreportal system update - Update LibrePortal to latest version" echo " libreportal system reset - Reinstall LibrePortal install files" echo " libreportal system reclaim - Reclaim Docker space (build cache + dangling images)" + echo " libreportal system image rm [--force] - Remove specific images (comma-separated ids)" echo "" } diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 877c11d..cbfacf0 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -676,6 +676,7 @@ declare -gA LP_FN_MAP=( [_reconcileSplitValueComment]="config/core/variables/config_scan_variables.sh" [reconcileWebuiDirOwnership]="function/permission/libreportal_folders.sh" [recoverOrphans]="crontab/task/crontab_task_processor.sh" + [removeDockerImages]="cli/commands/system/cli_system_commands.sh" [removeEmptyLineAtFileEnd]="function/file/empty_line/remove_line.sh" [repairDirectoryStructure]="crontab/task/crontab_check_processor.sh" [repairFileSystem]="crontab/task/crontab_check_processor.sh" @@ -884,6 +885,7 @@ declare -gA LP_FN_MAP=( [webuiSyncAppIcon]="webui/data/utils/webui_app_icons.sh" [webuiSyncAppIcons]="webui/data/utils/webui_app_icons.sh" [webuiSystemApps]="webui/data/generators/system/webui_system_metrics.sh" + [webuiSystemAppStorage]="webui/data/generators/system/webui_system_metrics.sh" [webuiSystemDisk]="webui/data/generators/system/webui_system_disk.sh" [webuiSystemInfo]="webui/data/generators/system/webui_system_info.sh" [webuiSystemMemory]="webui/data/generators/system/webui_system_memory.sh" @@ -1572,6 +1574,7 @@ declare -gA LP_FN_ROOT=( [_reconcileSplitValueComment]="scripts" [reconcileWebuiDirOwnership]="scripts" [recoverOrphans]="scripts" + [removeDockerImages]="scripts" [removeEmptyLineAtFileEnd]="scripts" [repairDirectoryStructure]="scripts" [repairFileSystem]="scripts" @@ -1780,6 +1783,7 @@ declare -gA LP_FN_ROOT=( [webuiSyncAppIcon]="scripts" [webuiSyncAppIcons]="scripts" [webuiSystemApps]="scripts" + [webuiSystemAppStorage]="scripts" [webuiSystemDisk]="scripts" [webuiSystemInfo]="scripts" [webuiSystemMemory]="scripts" @@ -2488,6 +2492,7 @@ reconcileDockerOwnership() { source "${install_scripts_dir}function/permission/l _reconcileSplitValueComment() { source "${install_scripts_dir}config/core/variables/config_scan_variables.sh"; _reconcileSplitValueComment "$@"; } reconcileWebuiDirOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileWebuiDirOwnership "$@"; } recoverOrphans() { source "${install_scripts_dir}crontab/task/crontab_task_processor.sh"; recoverOrphans "$@"; } +removeDockerImages() { source "${install_scripts_dir}cli/commands/system/cli_system_commands.sh"; removeDockerImages "$@"; } removeEmptyLineAtFileEnd() { source "${install_scripts_dir}function/file/empty_line/remove_line.sh"; removeEmptyLineAtFileEnd "$@"; } repairDirectoryStructure() { source "${install_scripts_dir}crontab/task/crontab_check_processor.sh"; repairDirectoryStructure "$@"; } repairFileSystem() { source "${install_scripts_dir}crontab/task/crontab_check_processor.sh"; repairFileSystem "$@"; } @@ -2696,6 +2701,7 @@ webuiSetConfigOptions() { source "${install_scripts_dir}webui/data/generators/co webuiSyncAppIcon() { source "${install_scripts_dir}webui/data/utils/webui_app_icons.sh"; webuiSyncAppIcon "$@"; } webuiSyncAppIcons() { source "${install_scripts_dir}webui/data/utils/webui_app_icons.sh"; webuiSyncAppIcons "$@"; } webuiSystemApps() { source "${install_scripts_dir}webui/data/generators/system/webui_system_metrics.sh"; webuiSystemApps "$@"; } +webuiSystemAppStorage() { source "${install_scripts_dir}webui/data/generators/system/webui_system_metrics.sh"; webuiSystemAppStorage "$@"; } webuiSystemDisk() { source "${install_scripts_dir}webui/data/generators/system/webui_system_disk.sh"; webuiSystemDisk "$@"; } webuiSystemInfo() { source "${install_scripts_dir}webui/data/generators/system/webui_system_info.sh"; webuiSystemInfo "$@"; } webuiSystemMemory() { source "${install_scripts_dir}webui/data/generators/system/webui_system_memory.sh"; webuiSystemMemory "$@"; }