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

SeaweedFS Block Storage

Dashboard Operations
` // blockDashContentHTML is the Dashboard tab content. const blockDashContentHTML = `
Block Servers
{{len .Servers}}
Total Volumes
{{.TotalVolumes}}
Active
{{.ActiveCount}}
Pending
{{.PendingCount}}
Total Capacity
{{.TotalSizeMB}} MB
Cluster Health
Barrier Lag LSN
{{.BarrierLagLSN}}
Promotions
{{.PromotionsTotal}}
Failovers
{{.FailoversTotal}}
Rebuilds
{{.RebuildsTotal}}
Queue Depth
{{.AssignmentQueueLen}}

Servers

{{if .Servers}} {{range .Servers}} {{end}}
AddressVolume CountBlock Capable
{{.Address}} {{.VolumeCount}} {{if .BlockCapable}}Yes{{else}}No{{end}}
{{else}}
No block servers connected
{{end}}

Volumes

{{if .Volumes}} {{range .Volumes}} {{end}}
NameServerSizeRoleStatus DurabilityHealthWAL LSNWAL LagDegraded EpochReplica
{{.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}}
{{else}}
No block volumes
{{end}}
` // blockOpsContentHTML is the Operations tab content. const blockOpsContentHTML = `

Create Volume

Volumes

{{if .Volumes}} {{range .Volumes}} {{end}}
NameServerSizeRoleStatus DurabilityHealthWAL LagDegraded EpochReplicaAction
{{.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}}
{{else}}
No block volumes
{{end}} `