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;
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
.sys-storage-stat-recl .sys-storage-stat-v { color: var(--status-warning); }
|
.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 {
|
.sys-storage-stat-sub {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: rgba(var(--text-rgb), 0.55);
|
color: rgba(var(--text-rgb), 0.55);
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
// - Top 10 volumes by size
|
// - Top 10 volumes by size
|
||||||
//
|
//
|
||||||
// One backend call (GET /api/system/storage), cached server-side for 5s.
|
// 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 {
|
class SystemStoragePage {
|
||||||
constructor(rootId = 'config-section') {
|
constructor(rootId = 'config-section') {
|
||||||
@ -47,6 +48,45 @@ class SystemStoragePage {
|
|||||||
const back = e.target.closest('[data-back]');
|
const back = e.target.closest('[data-back]');
|
||||||
if (back && window.navigateToRoute) {
|
if (back && window.navigateToRoute) {
|
||||||
window.navigateToRoute('/admin/config/system');
|
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>
|
<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>
|
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · stopped containers, dangling images, unused volumes & cache</span>
|
||||||
</div>
|
</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>
|
||||||
</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
|
* Create a task object
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -60,6 +60,9 @@ class TaskRouter {
|
|||||||
case 'system_update':
|
case 'system_update':
|
||||||
return await this.actions.systemUpdate();
|
return await this.actions.systemUpdate();
|
||||||
|
|
||||||
|
case 'system_reclaim':
|
||||||
|
return await this.actions.systemReclaim();
|
||||||
|
|
||||||
case 'tool':
|
case 'tool':
|
||||||
return await this.actions.runTool(params.appName, params.toolName, params.toolArgs, params.toolLabel);
|
return await this.actions.runTool(params.appName, params.toolName, params.toolArgs, params.toolLabel);
|
||||||
|
|
||||||
|
|||||||
@ -3,23 +3,48 @@
|
|||||||
# System Commands Handler
|
# System Commands Handler
|
||||||
# Handles all system subcommands by calling core functions
|
# 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()
|
cliHandleSystemCommands()
|
||||||
{
|
{
|
||||||
local action="$initial_command2"
|
local action="$initial_command2"
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
"status")
|
"status")
|
||||||
tagsValidateShowSystemStatus
|
tagsValidateShowSystemStatus
|
||||||
;;
|
;;
|
||||||
|
|
||||||
"update")
|
"update")
|
||||||
checkUpdates
|
checkUpdates
|
||||||
;;
|
;;
|
||||||
|
|
||||||
"reset")
|
"reset")
|
||||||
runReinstall
|
runReinstall
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
"reclaim")
|
||||||
|
reclaimDockerSpace
|
||||||
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
isNotice "Invalid system command: $action"
|
isNotice "Invalid system command: $action"
|
||||||
cliShowSystemHelp
|
cliShowSystemHelp
|
||||||
|
|||||||
@ -11,5 +11,6 @@ cliShowSystemHelp()
|
|||||||
echo " libreportal system status - Show overall system status"
|
echo " libreportal system status - Show overall system status"
|
||||||
echo " libreportal system update - Update LibrePortal to latest version"
|
echo " libreportal system update - Update LibrePortal to latest version"
|
||||||
echo " libreportal system reset - Reinstall LibrePortal install files"
|
echo " libreportal system reset - Reinstall LibrePortal install files"
|
||||||
|
echo " libreportal system reclaim - Reclaim Docker space (build cache + dangling images)"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user