package weed_server
import (
"html/template"
"net/http"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockapi"
)
var blockDashTemplate = template.Must(template.New("blockDash").Parse(blockLayoutHTML + blockDashContentHTML))
var blockOpsTemplate = template.Must(template.New("blockOps").Parse(blockLayoutHTML + blockOpsContentHTML))
type blockUIVolume struct {
blockapi.VolumeInfo
SizeMB uint64
WALHeadLSN uint64
MaxWALLag uint64 // max WAL lag across replicas
}
type blockUIData struct {
Tab string // "dashboard" or "ops"
Volumes []blockUIVolume
Servers []blockapi.ServerInfo
// Dashboard stats
TotalVolumes int
ActiveCount int
PendingCount int
TotalSizeMB uint64
// Observability (CP8-4)
BarrierLagLSN uint64
PromotionsTotal uint64
FailoversTotal uint64
RebuildsTotal uint64
AssignmentQueueLen int
}
func (ms *MasterServer) buildBlockUIData(tab string) blockUIData {
entries := ms.blockRegistry.ListAll()
volumes := make([]blockUIVolume, len(entries))
var totalSizeMB uint64
var activeCount, pendingCount int
for i := range entries {
e := &entries[i]
info := entryToVolumeInfo(e, ms.blockRegistry.IsBlockCapable(e.VolumeServer))
mb := info.SizeBytes / (1024 * 1024)
var maxLag uint64
for _, ri := range e.Replicas {
if ri.WALLag > maxLag {
maxLag = ri.WALLag
}
}
volumes[i] = blockUIVolume{
VolumeInfo: info,
SizeMB: mb,
WALHeadLSN: e.WALHeadLSN,
MaxWALLag: maxLag,
}
totalSizeMB += mb
if e.Status == StatusActive {
activeCount++
} else {
pendingCount++
}
}
summaries := ms.blockRegistry.ServerSummaries()
servers := make([]blockapi.ServerInfo, len(summaries))
for i, s := range summaries {
servers[i] = blockapi.ServerInfo{
Address: s.Address,
VolumeCount: s.VolumeCount,
BlockCapable: s.BlockCapable,
}
}
return blockUIData{
Tab: tab,
Volumes: volumes,
Servers: servers,
TotalVolumes: len(entries),
ActiveCount: activeCount,
PendingCount: pendingCount,
TotalSizeMB: totalSizeMB,
BarrierLagLSN: ms.blockRegistry.MaxBarrierLagLSN(),
PromotionsTotal: ms.blockRegistry.PromotionsTotal.Load(),
FailoversTotal: ms.blockRegistry.FailoversTotal.Load(),
RebuildsTotal: ms.blockRegistry.RebuildsTotal.Load(),
AssignmentQueueLen: ms.blockAssignmentQueue.TotalPending(),
}
}
// blockUIHandler serves the /block/ dashboard page.
func (ms *MasterServer) blockUIHandler(w http.ResponseWriter, r *http.Request) {
data := ms.buildBlockUIData("dashboard")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := blockDashTemplate.Execute(w, data); err != nil {
glog.V(0).Infof("block UI template error: %v", err)
}
}
// blockOpsHandler serves the /block/ops operations page.
func (ms *MasterServer) blockOpsHandler(w http.ResponseWriter, r *http.Request) {
data := ms.buildBlockUIData("ops")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := blockOpsTemplate.Execute(w, data); err != nil {
glog.V(0).Infof("block UI template error: %v", err)
}
}
// blockLayoutHTML is the shared HTML layout with tab navigation.
const blockLayoutHTML = `
SeaweedFS Block Storage
`
// blockDashContentHTML is the Dashboard tab content.
const blockDashContentHTML = `
Block Servers
{{len .Servers}}
Total Volumes
{{.TotalVolumes}}
Pending
{{.PendingCount}}
Total Capacity
{{.TotalSizeMB}} MB
Cluster Health
Barrier Lag LSN
{{.BarrierLagLSN}}
Promotions
{{.PromotionsTotal}}
Failovers
{{.FailoversTotal}}
Rebuilds
{{.RebuildsTotal}}
Queue Depth
{{.AssignmentQueueLen}}
Servers
{{if .Servers}}
| Address | Volume Count | Block Capable |
{{range .Servers}}
| {{.Address}} |
{{.VolumeCount}} |
{{if .BlockCapable}}Yes{{else}}No{{end}} |
{{end}}
{{else}}
No block servers connected
{{end}}
Volumes
{{if .Volumes}}
| Name | Server | Size | Role | Status |
Durability | Health | WAL LSN | WAL Lag | Degraded |
Epoch | Replica |
{{range .Volumes}}
| {{.Name}} |
{{.VolumeServer}} |
{{.SizeMB}} MB |
{{if eq .Role "primary"}}primary{{else if eq .Role "replica"}}replica{{else}}{{.Role}}{{end}} |
{{if eq .Status "active"}}active{{else}}{{.Status}}{{end}} |
{{if eq .DurabilityMode "sync_all"}}sync_all{{else if eq .DurabilityMode "sync_quorum"}}sync_quorum{{else}}best_effort{{end}} |
{{printf "%.2f" .HealthScore}} |
{{.WALHeadLSN}} |
{{.MaxWALLag}} |
{{if .ReplicaDegraded}}yes{{else}}-{{end}} |
{{.Epoch}} |
{{.ReplicaServer}} |
{{end}}
{{else}}
No block volumes
{{end}}
`
// blockOpsContentHTML is the Operations tab content.
const blockOpsContentHTML = `
Volumes
{{if .Volumes}}
| Name | Server | Size | Role | Status |
Durability | Health | WAL Lag | Degraded |
Epoch | Replica | Action |
{{range .Volumes}}
| {{.Name}} |
{{.VolumeServer}} |
{{.SizeMB}} MB |
{{if eq .Role "primary"}}primary{{else if eq .Role "replica"}}replica{{else}}{{.Role}}{{end}} |
{{if eq .Status "active"}}active{{else}}{{.Status}}{{end}} |
{{if eq .DurabilityMode "sync_all"}}sync_all{{else if eq .DurabilityMode "sync_quorum"}}sync_quorum{{else}}best_effort{{end}} |
{{printf "%.2f" .HealthScore}} |
{{.MaxWALLag}} |
{{if .ReplicaDegraded}}yes{{else}}-{{end}} |
{{.Epoch}} |
{{.ReplicaServer}} |
|
{{end}}
{{else}}
No block volumes
{{end}}