feat(system): "Reclaim space" action on the Storage page
Adds a `libreportal system reclaim` CLI command and an orange "Reclaim space" button on /admin/config/system/storage (the v2 prune control the page always hinted at). Scope is deliberately SAFE: build cache + dangling (untagged) images only (docker builder prune -f + docker image prune -f via the rootless-aware runFileOp). It never touches volumes (app data) or tagged/in-use images, so nothing an app relies on is removed. Wiring mirrors system_update: a systemReclaim() action + system_reclaim route case run the command verbatim through the task processor. The button confirms via showConfirmation, shows a spinner, and re-reads storage usage as the prune lands. Button styled with --status-warning to match the Reclaimable stat it sits under, with a note clarifying scope. Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
f6fecd023a
commit
3031c6cab9
@ -1009,6 +1009,47 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); }
|
||||
line-height: 1.05;
|
||||
}
|
||||
.sys-storage-stat-recl .sys-storage-stat-v { color: var(--status-warning); }
|
||||
|
||||
/* "Reclaim space" action — orange to match the Reclaimable stat it sits
|
||||
under. Safe prune only (build cache + dangling images). */
|
||||
.sys-storage-reclaim {
|
||||
align-self: flex-start;
|
||||
margin-top: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--status-warning);
|
||||
background: rgba(var(--status-warning-rgb), 0.12);
|
||||
border: 1px solid rgba(var(--status-warning-rgb), 0.4);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background .15s ease, border-color .15s ease, transform .15s ease;
|
||||
}
|
||||
.sys-storage-reclaim:hover {
|
||||
background: rgba(var(--status-warning-rgb), 0.2);
|
||||
border-color: rgba(var(--status-warning-rgb), 0.65);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.sys-storage-reclaim:disabled,
|
||||
.sys-storage-reclaim.is-running {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
transform: none;
|
||||
}
|
||||
.sys-storage-reclaim svg { flex: 0 0 auto; }
|
||||
.sys-storage-reclaim.is-running svg { animation: sysReclaimSpin 0.8s linear infinite; }
|
||||
.sys-storage-reclaim-note {
|
||||
margin-top: 6px;
|
||||
font-size: 0.72rem;
|
||||
color: rgba(var(--text-rgb), 0.5);
|
||||
}
|
||||
@keyframes sysReclaimSpin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sys-storage-reclaim.is-running svg { animation: none; }
|
||||
}
|
||||
.sys-storage-stat-sub {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(var(--text-rgb), 0.55);
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
// - Top 10 volumes by size
|
||||
//
|
||||
// One backend call (GET /api/system/storage), cached server-side for 5s.
|
||||
// No actions yet — purely informational. Prune controls deferred to a v2.
|
||||
// A "Reclaim space" button runs the safe prune (build cache + dangling
|
||||
// images, never volumes) via the system_reclaim task, then re-reads usage.
|
||||
|
||||
class SystemStoragePage {
|
||||
constructor(rootId = 'config-section') {
|
||||
@ -47,6 +48,45 @@ class SystemStoragePage {
|
||||
const back = e.target.closest('[data-back]');
|
||||
if (back && window.navigateToRoute) {
|
||||
window.navigateToRoute('/admin/config/system');
|
||||
return;
|
||||
}
|
||||
const recl = e.target.closest('[data-storage-reclaim]');
|
||||
if (recl) { this._reclaim(recl); }
|
||||
}
|
||||
|
||||
// Confirm, then run the safe reclaim task and re-read usage as it lands.
|
||||
_reclaim(btn) {
|
||||
const run = async () => {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('is-running');
|
||||
try {
|
||||
if (!window.tasksManager?.router?.routeAction) {
|
||||
throw new Error('Task system not ready');
|
||||
}
|
||||
await window.tasksManager.router.routeAction('system_reclaim');
|
||||
window.notificationSystem?.show?.('Reclaiming Docker space…', 'info');
|
||||
// The prune runs in the background task processor; re-read
|
||||
// usage a couple of times so the numbers settle without a
|
||||
// manual refresh. Each _render rebuilds the button fresh.
|
||||
setTimeout(() => this._load().then(() => this._render()), 3000);
|
||||
setTimeout(() => this._load().then(() => this._render()), 8000);
|
||||
} catch (err) {
|
||||
window.notificationSystem?.show?.(`Reclaim failed: ${err.message || err}`, 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('is-running');
|
||||
}
|
||||
};
|
||||
if (window.showConfirmation) {
|
||||
window.showConfirmation(
|
||||
'Reclaim Docker space',
|
||||
'Remove the build cache and dangling (untagged) images? Volumes, app data, and images currently in use are left untouched.',
|
||||
run,
|
||||
'Reclaim space',
|
||||
'Cancel',
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +205,11 @@ class SystemStoragePage {
|
||||
<strong class="sys-storage-stat-v">${fmt.bytes(recl)}</strong>
|
||||
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · stopped containers, dangling images, unused volumes & cache</span>
|
||||
</div>
|
||||
<button type="button" class="sys-storage-reclaim" data-storage-reclaim title="Remove build cache and dangling images (volumes and in-use images are kept)">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
Reclaim space
|
||||
</button>
|
||||
<span class="sys-storage-reclaim-note">Frees build cache & dangling images · volumes and in-use images are kept</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
@ -231,6 +231,17 @@ async configUpdate(changes) {
|
||||
}
|
||||
}
|
||||
|
||||
async systemReclaim() {
|
||||
try {
|
||||
// Full command passed verbatim (executeTask uses it as-is when it
|
||||
// starts with 'libreportal'), so it routes to the CLI's system handler.
|
||||
await this.executeTask('system_reclaim', 'system', 'libreportal system reclaim');
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to reclaim space: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a task object
|
||||
*/
|
||||
|
||||
@ -60,6 +60,9 @@ class TaskRouter {
|
||||
case 'system_update':
|
||||
return await this.actions.systemUpdate();
|
||||
|
||||
case 'system_reclaim':
|
||||
return await this.actions.systemReclaim();
|
||||
|
||||
case 'tool':
|
||||
return await this.actions.runTool(params.appName, params.toolName, params.toolArgs, params.toolLabel);
|
||||
|
||||
|
||||
@ -3,6 +3,27 @@
|
||||
# System Commands Handler
|
||||
# Handles all system subcommands by calling core functions
|
||||
|
||||
# Reclaim Docker disk space — SAFE scope only: build cache + dangling
|
||||
# (untagged) images. Never prunes volumes (app data) or tagged/in-use images,
|
||||
# so nothing an app relies on is removed. runFileOp targets the correct daemon
|
||||
# (rootless: as the install user with DOCKER_HOST set).
|
||||
reclaimDockerSpace()
|
||||
{
|
||||
isHeader "Reclaiming Docker Space"
|
||||
isNotice "Safe scope: build cache + dangling images only (no volumes, no in-use images)."
|
||||
|
||||
local cache_out image_out
|
||||
cache_out=$(runFileOp docker builder prune -f 2>&1)
|
||||
checkSuccess "Pruned build cache"
|
||||
echo "$cache_out" | grep -i "Total:" | sed 's/^/ /'
|
||||
|
||||
image_out=$(runFileOp docker image prune -f 2>&1)
|
||||
checkSuccess "Pruned dangling images"
|
||||
echo "$image_out" | grep -i "Total reclaimed space" | sed 's/^/ /'
|
||||
|
||||
isSuccessful "Reclaim complete"
|
||||
}
|
||||
|
||||
cliHandleSystemCommands()
|
||||
{
|
||||
local action="$initial_command2"
|
||||
@ -20,6 +41,10 @@ cliHandleSystemCommands()
|
||||
runReinstall
|
||||
;;
|
||||
|
||||
"reclaim")
|
||||
reclaimDockerSpace
|
||||
;;
|
||||
|
||||
*)
|
||||
isNotice "Invalid system command: $action"
|
||||
cliShowSystemHelp
|
||||
|
||||
@ -11,5 +11,6 @@ cliShowSystemHelp()
|
||||
echo " libreportal system status - Show overall system status"
|
||||
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 ""
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user