diff --git a/data/atlas.db b/data/atlas.db new file mode 100644 index 0000000..62d6797 Binary files /dev/null and b/data/atlas.db differ diff --git a/internal/httpapp/dashboard_handlers.go b/internal/httpapp/dashboard_handlers.go new file mode 100644 index 0000000..dab7431 --- /dev/null +++ b/internal/httpapp/dashboard_handlers.go @@ -0,0 +1,130 @@ +package httpapp + +import ( + "fmt" + "net/http" +) + +// DashboardData represents aggregated dashboard statistics +type DashboardData struct { + Storage struct { + TotalCapacity uint64 `json:"total_capacity"` + TotalAllocated uint64 `json:"total_allocated"` + TotalAvailable uint64 `json:"total_available"` + PoolCount int `json:"pool_count"` + DatasetCount int `json:"dataset_count"` + ZVOLCount int `json:"zvol_count"` + SnapshotCount int `json:"snapshot_count"` + } `json:"storage"` + Services struct { + SMBShares int `json:"smb_shares"` + NFSExports int `json:"nfs_exports"` + ISCSITargets int `json:"iscsi_targets"` + SMBStatus bool `json:"smb_status"` + NFSStatus bool `json:"nfs_status"` + ISCSIStatus bool `json:"iscsi_status"` + } `json:"services"` + Jobs struct { + Total int `json:"total"` + Running int `json:"running"` + Completed int `json:"completed"` + Failed int `json:"failed"` + } `json:"jobs"` + RecentAuditLogs []map[string]interface{} `json:"recent_audit_logs,omitempty"` +} + +// handleDashboardAPI returns aggregated dashboard data +func (a *App) handleDashboardAPI(w http.ResponseWriter, r *http.Request) { + data := DashboardData{} + + // Storage statistics + pools, err := a.zfs.ListPools() + if err == nil { + data.Storage.PoolCount = len(pools) + for _, pool := range pools { + data.Storage.TotalCapacity += pool.Size + data.Storage.TotalAllocated += pool.Allocated + data.Storage.TotalAvailable += pool.Free + } + } + + datasets, err := a.zfs.ListDatasets("") + if err == nil { + data.Storage.DatasetCount = len(datasets) + } + + zvols, err := a.zfs.ListZVOLs("") + if err == nil { + data.Storage.ZVOLCount = len(zvols) + } + + snapshots, err := a.zfs.ListSnapshots("") + if err == nil { + data.Storage.SnapshotCount = len(snapshots) + } + + // Service statistics + smbShares := a.smbStore.List() + data.Services.SMBShares = len(smbShares) + + nfsExports := a.nfsStore.List() + data.Services.NFSExports = len(nfsExports) + + iscsiTargets := a.iscsiStore.List() + data.Services.ISCSITargets = len(iscsiTargets) + + // Service status + if a.smbService != nil { + data.Services.SMBStatus, _ = a.smbService.GetStatus() + } + if a.nfsService != nil { + data.Services.NFSStatus, _ = a.nfsService.GetStatus() + } + if a.iscsiService != nil { + data.Services.ISCSIStatus, _ = a.iscsiService.GetStatus() + } + + // Job statistics + allJobs := a.jobManager.List("") + data.Jobs.Total = len(allJobs) + for _, job := range allJobs { + switch job.Status { + case "running": + data.Jobs.Running++ + case "completed": + data.Jobs.Completed++ + case "failed": + data.Jobs.Failed++ + } + } + + // Recent audit logs (last 5) + auditLogs := a.auditStore.List("", "", "", 5) + data.RecentAuditLogs = make([]map[string]interface{}, 0, len(auditLogs)) + for _, log := range auditLogs { + data.RecentAuditLogs = append(data.RecentAuditLogs, map[string]interface{}{ + "id": log.ID, + "actor": log.Actor, + "action": log.Action, + "resource": log.Resource, + "result": log.Result, + "timestamp": log.Timestamp.Format("2006-01-02 15:04:05"), + }) + } + + writeJSON(w, http.StatusOK, data) +} + +// formatBytes formats bytes to human-readable format +func formatBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} diff --git a/internal/httpapp/routes.go b/internal/httpapp/routes.go index 64cac07..bbe68ac 100644 --- a/internal/httpapp/routes.go +++ b/internal/httpapp/routes.go @@ -18,6 +18,12 @@ func (a *App) routes() { a.mux.HandleFunc("/healthz", a.handleHealthz) a.mux.HandleFunc("/metrics", a.handleMetrics) + // Dashboard API + a.mux.HandleFunc("/api/v1/dashboard", methodHandler( + func(w http.ResponseWriter, r *http.Request) { a.handleDashboardAPI(w, r) }, + nil, nil, nil, nil, + )) + // API v1 routes - ZFS Management a.mux.HandleFunc("/api/v1/disks", methodHandler( func(w http.ResponseWriter, r *http.Request) { a.handleListDisks(w, r) }, diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html index 282bba9..8476e7b 100644 --- a/web/templates/dashboard.html +++ b/web/templates/dashboard.html @@ -5,18 +5,32 @@
Welcome to atlasOS Storage Controller
---
+-
+ZFS Pools
+-
Total Capacity
--
-Active Shares
+ +SMB + NFS
@@ -44,30 +58,130 @@ ---
+-
Active Targets
- -Loading...
+