Files
seaweedfs/weed/admin/view/app/admin.templ
T
Chris LuandClaude Opus 5.5 7beac0e8ea admin: show disk usage on the dashboard instead of the data size (#12466)
* admin: show disk usage on the dashboard instead of the data size

The dashboard's Total Size card summed the volume and EC shard sizes the
volume servers report. That leaves out deleted needles waiting for vacuum,
EC generations kept after a vacuum, and anything else on the disks, so on a
cluster with a lot of EC vacuum activity it showed far less than the disks
held.

The card now shows the bytes in use on the volume servers' disks, the same
statfs numbers the volume server list uses, with the disks' capacity, and
its trend line follows that number. Servers that report no statfs numbers
count their data size, as on the volume server list.

Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi

* admin: compute the disk usage percentage in float64

current*100 overflows int64 past about 92 PB of used space, so the
dashboard card showed a negative percentage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* admin: size the volume server disk bar with calculatePercent64

int(host.DiskUsage) truncates byte counts on 32-bit builds, so the bar
width was wrong for any disk over 2 GB there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* admin: test the cluster disk totals across statfs and fallback servers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:25:10 -08:00

675 lines
36 KiB
Templ

package app
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ Admin(data dash.AdminData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="bi bi-speedometer2 me-2"></i>Dashboard
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href={ dash.PUrl(ctx, "/object-store/buckets") } class="btn btn-sm btn-primary">
<i class="bi bi-box me-1"></i>Object Store Buckets
</a>
</div>
</div>
</div>
<div id="dashboard-content">
<!-- Version mismatch warning: nodes reporting different binary versions
usually means an upgrade did not reach every server. -->
if data.VersionSummary.Mixed() {
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
<div>
<strong>Mixed cluster versions detected.</strong>
Not all servers are running the same SeaweedFS version — some servers may not have been upgraded.
<ul class="mb-0 mt-1">
for _, nt := range data.VersionSummary.NodeTypes {
<li>{nt.Type}: {nt.Spread()}</li>
}
</ul>
</div>
</div>
}
<!-- Summary Cards — each carries an inline sparkline of recent history
from the admin's own snapshots (no Prometheus required). -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Total Volumes
</div>
<div class="h5 mb-0 font-weight-bold text-body-emphasis">
{fmt.Sprintf("%d", data.TotalVolumes)}
</div>
</div>
<div class="col-auto">
<i class="bi bi-database icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Volumes)
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1" title="Chunks stored in volumes. A file is split into one or more chunks, so this is not the number of files.">
Total Chunks
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{formatNumber(data.TotalChunks)}
</div>
</div>
<div class="col-auto">
<i class="bi bi-boxes icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Chunks)
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Disk Usage
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{formatBytes(data.DiskUsage)}
</div>
if data.DiskCapacity > 0 {
<div class="text-xs text-muted">
of {formatBytes(data.DiskCapacity)} ({fmt.Sprintf("%d%%", calculatePercent64(data.DiskUsage, data.DiskCapacity))})
</div>
}
</div>
<div class="col-auto">
<i class="bi bi-device-hdd icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.DiskUsed)
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Volume Size Limit
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d MB", data.VolumeSizeLimitMB)}
</div>
</div>
<div class="col-auto">
<i class="bi bi-gear-wide-connected icon-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Second Row for EC Shards Information -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-secondary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
EC Volumes
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalEcVolumes)}
</div>
</div>
<div class="col-auto">
<i class="bi bi-layers icon-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-dark shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
EC Shards
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalEcShards)}
</div>
</div>
<div class="col-auto">
<i class="bi bi-puzzle icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.EcShards)
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Active Tasks
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ data.Trends.TasksValue }
</div>
</div>
<div class="col-auto">
<i class="bi bi-list-check icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Tasks)
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Workers
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ data.Trends.WorkersValue }
</div>
</div>
<div class="col-auto">
<i class="bi bi-people-fill icon-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Workers)
</div>
</div>
</div>
</div>
<!-- Storage Tiers -->
if len(data.TierStats) > 0 {
<div class="row mb-4">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-layers me-2"></i>Storage Tiers
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Tier</th>
<th>Volumes</th>
<th>EC Shards</th>
<th>Data Size</th>
<th>Disk Used</th>
<th>Capacity</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
for _, tier := range data.TierStats {
<tr>
<td>
if tier.IsRemote {
<i class="bi bi-cloud me-1 text-info"></i>{tier.Name}
<span class="badge bg-info text-white ms-1">remote</span>
} else {
<i class="bi bi-device-hdd me-1 text-muted"></i>{tier.Name}
}
</td>
<td>{fmt.Sprintf("%d", tier.VolumeCount)}</td>
<td>
if tier.IsRemote {
<span class="text-muted">-</span>
} else {
{fmt.Sprintf("%d", tier.EcShardCount)}
}
</td>
<td>{formatBytes(tier.DataSize)}</td>
<td>
if tier.IsRemote || tier.DiskUsed == 0 {
<span class="text-muted">-</span>
} else {
{formatBytes(tier.DiskUsed)}
}
</td>
<td>
if tier.IsRemote {
<span class="text-muted">-</span>
} else {
{formatBytes(tier.DiskCapacity)}
}
</td>
<td>
if tier.IsRemote {
<span class="text-muted">-</span>
} else {
<div class="d-flex align-items-center">
<div class="progress flex-grow-1" style="height: 20px; min-width: 48px;">
<div class="progress-bar" role="progressbar"
style={fmt.Sprintf("width: %d%%", tier.UsagePercent())}>
</div>
</div>
<span class="ms-2 text-nowrap">{fmt.Sprintf("%d%%", tier.UsagePercent())}</span>
</div>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
}
<!-- Mount Clients -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-plug me-2"></i>Mount Clients
</h6>
<a href={ dash.PUrl(ctx, "/cluster/mount-clients") } class="btn btn-sm btn-outline-primary">
View Details <i class="bi bi-arrow-right ms-1"></i>
</a>
</div>
<div class="card-body">
<div class="d-flex align-items-center">
<i class="bi bi-plug icon-2x text-gray-300 me-3"></i>
<div>
<div class="h4 mb-0 font-weight-bold text-gray-800">{fmt.Sprintf("%d", data.TotalMountClients)}</div>
<div class="text-xs text-muted text-uppercase">Connected FUSE/VFS mount clients</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Master Nodes Status -->
<div class="row mb-4">
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-server me-2"></i>Master Nodes
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" width="100%" cellspacing="0">
<thead>
<tr>
<th>Address</th>
<th>Role</th>
</tr>
</thead>
<tbody>
for _, master := range data.MasterNodes {
<tr>
<td>
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", master.Address))} target="_blank">
{master.Address}
<i class="bi bi-box-arrow-up-right ms-1 text-muted"></i>
</a>
</td>
<td>
if master.IsLeader {
<span class="badge bg-primary">Leader</span>
} else {
<span class="badge bg-secondary">Follower</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- System Health -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-pie-chart me-2"></i>Cluster
</h6>
</div>
<div class="card-body text-center">
<div class="row">
<div class="col">
<div class="card bg-light">
<div class="card-body">
<h5>{fmt.Sprintf("%d", len(data.MasterNodes))}</h5>
<small class="text-muted">Masters</small>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light">
<div class="card-body">
<h5>{fmt.Sprintf("%d", len(data.VolumeServers))}</h5>
<small class="text-muted">Volume Servers</small>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light">
<div class="card-body">
<h5>{fmt.Sprintf("%d", len(data.FilerNodes))}</h5>
<small class="text-muted">Filers</small>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light">
<div class="card-body">
<h5>{fmt.Sprintf("%d", len(data.S3Nodes))}</h5>
<small class="text-muted">S3 Servers</small>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light">
<div class="card-body">
<h5>{fmt.Sprintf("%d", len(data.MessageBrokers))}</h5>
<small class="text-muted">Message Brokers</small>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light">
<div class="card-body">
if data.VersionSummary.Uniform {
<h5 class="text-truncate" title={data.VersionSummary.Version}>{data.VersionSummary.Version}</h5>
<small class="text-muted">Version</small>
} else if data.VersionSummary.Mixed() {
<h5 class="text-warning">
<i class="bi bi-exclamation-triangle-fill"></i>
</h5>
<small class="text-muted">Mixed Versions</small>
} else {
<h5 class="text-truncate">—</h5>
<small class="text-muted">Version</small>
}
</div>
</div>
</div>
</div>
if data.VersionSummary.Mixed() {
<div class="row mt-3">
<div class="col-12 text-start">
<small class="text-muted text-uppercase fw-bold">Node Versions</small>
for _, nt := range data.VersionSummary.NodeTypes {
<div class="d-flex justify-content-between border-bottom py-1">
<span>{nt.Type}</span>
<span>{nt.Spread()}</span>
</div>
}
</div>
</div>
}
</div>
</div>
</div>
</div>
<!-- Volume Servers -->
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-database me-2"></i>Volume Servers
</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical icon-sm icon-fixed-width text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
<div class="dropdown-header">Actions:</div>
<a class="dropdown-item" href={ dash.PUrl(ctx, "/volumes") }>View Details</a>
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster") }>Topology View</a>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Address</th>
<th>Data Center</th>
<th>Rack</th>
<th>Version</th>
<th>Volumes</th>
<th>EC Shards</th>
<th>Usage / Capacity</th>
</tr>
</thead>
<tbody>
for _, vs := range data.VolumeServers {
<tr>
<td>{vs.ID}</td>
<td>
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", vs.GetDisplayAddress()))} target="_blank">
{vs.GetDisplayAddress()}
<i class="bi bi-box-arrow-up-right ms-1 text-muted"></i>
</a>
</td>
<td>{vs.DataCenter}</td>
<td>{vs.Rack}</td>
<td title={vs.Version}>{dash.ShortVersion(vs.Version)}</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1" style="height: 20px; min-width: 48px;">
<div class="progress-bar" role="progressbar"
style={fmt.Sprintf("width: %d%%", calculatePercent(vs.Volumes, vs.MaxVolumes))}>
</div>
</div>
<span class="ms-2 text-nowrap">{fmt.Sprintf("%d/%d", vs.Volumes, vs.MaxVolumes)}</span>
</div>
</td>
<td>
if vs.EcShards > 0 {
<span class="badge bg-info text-white me-1">{fmt.Sprintf("%d", vs.EcShards)}</span>
if vs.EcVolumes > 0 {
<small class="text-muted">({fmt.Sprintf("%d vol", vs.EcVolumes)})</small>
}
} else {
<span class="text-muted">-</span>
}
</td>
<td>
{formatBytes(vs.DiskUsage)} / {formatBytes(vs.DiskCapacity)}
if vs.RemoteSize > 0 {
<br/>
<small class="text-muted"><i class="bi bi-cloud me-1"></i>{formatBytes(vs.RemoteSize)} remote</small>
}
</td>
</tr>
}
if len(data.VolumeServers) == 0 {
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="bi bi-info-circle me-2"></i>
No volume servers found
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Filer Nodes -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-folder me-2"></i>Filer Nodes
</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical icon-sm icon-fixed-width text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
<div class="dropdown-header">Actions:</div>
<a class="dropdown-item" href={ dash.PUrl(ctx, "/filer") }>File Browser</a>
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster") }>Topology View</a>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Address</th>
<th>Data Center</th>
<th>Rack</th>
<th>Version</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
for _, filer := range data.FilerNodes {
<tr>
<td>
<a href={templ.SafeURL(fmt.Sprintf("//%s", filer.Address))} target="_blank">
{filer.Address}
<i class="bi bi-box-arrow-up-right ms-1 text-muted"></i>
</a>
</td>
<td>{filer.DataCenter}</td>
<td>{filer.Rack}</td>
<td title={filer.Version}>{dash.ShortVersion(filer.Version)}</td>
<td>{filer.LastUpdated.Format("2006-01-02 15:04:05")}</td>
</tr>
}
if len(data.FilerNodes) == 0 {
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-info-circle me-2"></i>
No filer nodes found
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- S3 Servers -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-cloud me-2"></i>S3 Servers
</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical icon-sm icon-fixed-width text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
<div class="dropdown-header">Actions:</div>
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster/s3") }>View Details</a>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Address</th>
<th>Data Center</th>
<th>Version</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
for _, s3Node := range data.S3Nodes {
<tr>
<td>{s3Node.Address}</td>
<td>{s3Node.DataCenter}</td>
<td title={s3Node.Version}>{dash.ShortVersion(s3Node.Version)}</td>
<td>{s3Node.LastUpdated.Format("2006-01-02 15:04:05")}</td>
</tr>
}
if len(data.S3Nodes) == 0 {
<tr>
<td colspan="4" class="text-center text-muted py-4">
<i class="bi bi-info-circle me-2"></i>
No S3 servers found
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Last Updated -->
<div class="row">
<div class="col-12">
<small class="text-muted">
<i class="bi bi-clock me-1"></i>
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
</small>
</div>
</div>
</div>
}