Merge claude/2
This commit is contained in:
commit
a31d77b751
@ -1132,6 +1132,50 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); }
|
||||
color: rgba(var(--text-rgb), 0.5);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
/* Expandable per-app folder breakdown. */
|
||||
.sys-storage-app-row.is-clickable { cursor: pointer; }
|
||||
.sys-storage-caret,
|
||||
.sys-storage-caret-spacer {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-right: 6px;
|
||||
color: rgba(var(--text-rgb), 0.45);
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
.sys-storage-caret.is-open { transform: rotate(90deg); }
|
||||
.sys-storage-app-detail.is-collapsed { display: none; }
|
||||
.sys-storage-app-detail > td {
|
||||
padding: 2px 12px 10px 30px;
|
||||
background: rgba(var(--text-rgb), 0.02);
|
||||
}
|
||||
.sys-storage-folders { list-style: none; margin: 0; padding: 0; }
|
||||
.sys-storage-folders li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 0;
|
||||
font-size: 0.82rem;
|
||||
color: rgba(var(--text-rgb), 0.72);
|
||||
}
|
||||
.sys-storage-folder-path {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sys-storage-folder-size {
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: rgba(var(--text-rgb), 0.92);
|
||||
}
|
||||
.sys-storage-ext-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.68rem;
|
||||
background: rgba(var(--status-warning-rgb), 0.18);
|
||||
color: var(--status-warning);
|
||||
}
|
||||
.sys-storage-card-meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
|
||||
@ -19,6 +19,7 @@ class SystemStoragePage {
|
||||
this.rootId = rootId;
|
||||
this.data = null;
|
||||
this._timer = null;
|
||||
this._expanded = new Set(); // app names whose folder breakdown is open
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
@ -53,7 +54,19 @@ class SystemStoragePage {
|
||||
return;
|
||||
}
|
||||
const recl = e.target.closest('[data-storage-reclaim]');
|
||||
if (recl) { this._reclaim(recl); }
|
||||
if (recl) { this._reclaim(recl); 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]');
|
||||
if (tog) {
|
||||
const app = tog.getAttribute('data-app-toggle');
|
||||
if (this._expanded.has(app)) this._expanded.delete(app);
|
||||
else this._expanded.add(app);
|
||||
const open = this._expanded.has(app);
|
||||
const detail = this.root()?.querySelector(`[data-app-detail="${CSS.escape(app)}"]`);
|
||||
if (detail) detail.classList.toggle('is-collapsed', !open);
|
||||
tog.querySelector('.sys-storage-caret')?.classList.toggle('is-open', open);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm, then run the safe reclaim task and re-read usage as it lands.
|
||||
@ -270,6 +283,7 @@ class SystemStoragePage {
|
||||
const appRows = (as && Array.isArray(as.apps)) ? as.apps : [];
|
||||
const appMax = appRows.reduce((m, a) => Math.max(m, a.bytes || 0), 0);
|
||||
const hasExternal = appRows.some(a => a.external_bytes > 0);
|
||||
const cols = hasExternal ? 4 : 3;
|
||||
const appBody = appRows.length
|
||||
? `<div class="sys-apps-wrap">
|
||||
<table class="sys-apps">
|
||||
@ -277,12 +291,25 @@ class SystemStoragePage {
|
||||
<tbody>
|
||||
${appRows.map(a => {
|
||||
const pct = appMax ? ((a.bytes || 0) / appMax) * 100 : 0;
|
||||
return `<tr>
|
||||
<td class="sys-app-name">${fmt.escape(a.app)}</td>
|
||||
const mounts = Array.isArray(a.mounts) ? a.mounts : [];
|
||||
const open = this._expanded.has(a.app);
|
||||
// Each app row expands into its folders (the bind mounts
|
||||
// it stores data in), labelled by the in-container path.
|
||||
const folders = mounts.map(m => `
|
||||
<li>
|
||||
<span class="sys-storage-folder-path" title="${fmt.escape(m.source || '')}">${fmt.escape(m.path || m.source || '—')}</span>
|
||||
${m.external ? '<span class="sys-storage-ext-badge">external</span>' : ''}
|
||||
<span class="sys-storage-folder-size">${fmt.bytes(m.bytes || 0)}</span>
|
||||
</li>`).join('');
|
||||
return `<tr class="sys-storage-app-row${mounts.length ? ' is-clickable' : ''}"${mounts.length ? ` data-app-toggle="${fmt.escape(a.app)}"` : ''}>
|
||||
<td class="sys-app-name">${mounts.length ? `<span class="sys-storage-caret${open ? ' is-open' : ''}">▸</span>` : '<span class="sys-storage-caret-spacer"></span>'}${fmt.escape(a.app)}</td>
|
||||
<td>${fmt.bytes(a.bytes || 0)}</td>
|
||||
<td class="sys-storage-app-barcol"><span class="sys-storage-app-bar"><span style="width:${pct.toFixed(1)}%"></span></span></td>
|
||||
${hasExternal ? `<td>${a.external_bytes ? fmt.bytes(a.external_bytes) : '—'}</td>` : ''}
|
||||
</tr>`;
|
||||
</tr>${mounts.length ? `
|
||||
<tr class="sys-storage-app-detail${open ? '' : ' is-collapsed'}" data-app-detail="${fmt.escape(a.app)}">
|
||||
<td colspan="${cols}"><ul class="sys-storage-folders">${folders}</ul></td>
|
||||
</tr>` : ''}`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -236,18 +236,21 @@ webuiSystemAppStorage() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# name|project|source for every bind mount. Compose project groups an app's
|
||||
# containers; fall back to the container name when unlabelled (as webuiSystemApps).
|
||||
# name|project|destination|source for every bind mount. Compose project
|
||||
# groups an app's containers; fall back to the container name when unlabelled
|
||||
# (as webuiSystemApps). The destination (container path, e.g. /var/lib/mysql)
|
||||
# labels each folder in the per-app breakdown.
|
||||
local raw
|
||||
raw=$(runFileOp docker inspect $ids --format \
|
||||
'{{$n := .Name}}{{$p := index .Config.Labels "com.docker.compose.project"}}{{range .Mounts}}{{if eq .Type "bind"}}{{$n}}|{{$p}}|{{.Source}}
|
||||
'{{$n := .Name}}{{$p := index .Config.Labels "com.docker.compose.project"}}{{range .Mounts}}{{if eq .Type "bind"}}{{$n}}|{{$p}}|{{.Destination}}|{{.Source}}
|
||||
{{end}}{{end}}' 2>/dev/null)
|
||||
|
||||
declare -A app_local app_ext seen
|
||||
local total_local=0 total_ext=0
|
||||
# One JSON object per included mount; jq groups them into apps below.
|
||||
declare -A seen
|
||||
local mounts_stream=""
|
||||
|
||||
local cname proj src
|
||||
while IFS='|' read -r cname proj src; do
|
||||
local cname proj dest src
|
||||
while IFS='|' read -r cname proj dest src; do
|
||||
[[ -z "$src" ]] && continue
|
||||
cname="${cname#/}"
|
||||
proj="${proj:-$cname}"
|
||||
@ -266,35 +269,30 @@ webuiSystemAppStorage() {
|
||||
local bytes
|
||||
bytes=$(runFileOp du -sxB1 -- "$src" 2>/dev/null | awk '{print $1+0; exit}')
|
||||
bytes="${bytes:-0}"
|
||||
if [[ -n "$root_dev" && "$dev" == "$root_dev" ]]; then
|
||||
app_local["$proj"]=$(( ${app_local["$proj"]:-0} + bytes ))
|
||||
total_local=$(( total_local + bytes ))
|
||||
else
|
||||
app_ext["$proj"]=$(( ${app_ext["$proj"]:-0} + bytes ))
|
||||
total_ext=$(( total_ext + bytes ))
|
||||
fi
|
||||
local ext="false"
|
||||
[[ -n "$root_dev" && "$dev" == "$root_dev" ]] || ext="true"
|
||||
mounts_stream+="$(jq -nc --arg app "$proj" --arg path "$dest" --arg source "$src" \
|
||||
--argjson bytes "$bytes" --argjson external "$ext" \
|
||||
'{app:$app, path:$path, source:$source, bytes:$bytes, external:$external}')"$'\n'
|
||||
done <<< "$raw"
|
||||
|
||||
# Group mounts → apps (each with its folder breakdown), largest first.
|
||||
local apps_json="[]"
|
||||
local appnames
|
||||
appnames=$(printf '%s\n' "${!app_local[@]}" "${!app_ext[@]}" 2>/dev/null | sort -u)
|
||||
if [[ -n "$appnames" ]]; then
|
||||
local stream="" p
|
||||
while IFS= read -r p; do
|
||||
[[ -z "$p" ]] && continue
|
||||
local l="${app_local[$p]:-0}" e="${app_ext[$p]:-0}"
|
||||
stream+="$(jq -nc --arg app "$p" --argjson l "$l" --argjson e "$e" \
|
||||
'{app:$app, local_bytes:$l, external_bytes:$e, bytes:($l+$e)}')"$'\n'
|
||||
done <<< "$appnames"
|
||||
apps_json=$(printf '%s' "$stream" | jq -s 'sort_by(-.bytes)' 2>/dev/null)
|
||||
if [[ -n "${mounts_stream//[$' \t\n']/}" ]]; then
|
||||
apps_json=$(printf '%s' "$mounts_stream" | jq -s '
|
||||
group_by(.app) | map({
|
||||
app: .[0].app,
|
||||
bytes: (map(.bytes) | add),
|
||||
local_bytes: ([.[] | select(.external | not) | .bytes] | add // 0),
|
||||
external_bytes: ([.[] | select(.external) | .bytes] | add // 0),
|
||||
mounts: (sort_by(-.bytes) | map({path, source, bytes, external}))
|
||||
}) | sort_by(-.bytes)' 2>/dev/null)
|
||||
[[ -z "$apps_json" ]] && apps_json="[]"
|
||||
fi
|
||||
|
||||
local tmp; tmp=$(mktemp)
|
||||
if jq -n --argjson apps "$apps_json" \
|
||||
--argjson tl "$total_local" --argjson te "$total_ext" \
|
||||
--arg now "$now_iso" \
|
||||
'{apps:$apps, total_local:$tl, total_external:$te, total:($tl+$te), updated:$now}' \
|
||||
if printf '%s' "$apps_json" | jq --arg now "$now_iso" \
|
||||
'{apps: ., total: (map(.bytes) | add // 0), total_local: (map(.local_bytes) | add // 0), total_external: (map(.external_bytes) | add // 0), updated: $now}' \
|
||||
> "$tmp" 2>/dev/null; then
|
||||
runFileWrite "$final_file" < "$tmp"
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user