This commit is contained in:
BIN
data/atlas.db
Normal file
BIN
data/atlas.db
Normal file
Binary file not shown.
130
internal/httpapp/dashboard_handlers.go
Normal file
130
internal/httpapp/dashboard_handlers.go
Normal file
@@ -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])
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@ func (a *App) routes() {
|
|||||||
a.mux.HandleFunc("/healthz", a.handleHealthz)
|
a.mux.HandleFunc("/healthz", a.handleHealthz)
|
||||||
a.mux.HandleFunc("/metrics", a.handleMetrics)
|
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
|
// API v1 routes - ZFS Management
|
||||||
a.mux.HandleFunc("/api/v1/disks", methodHandler(
|
a.mux.HandleFunc("/api/v1/disks", methodHandler(
|
||||||
func(w http.ResponseWriter, r *http.Request) { a.handleListDisks(w, r) },
|
func(w http.ResponseWriter, r *http.Request) { a.handleListDisks(w, r) },
|
||||||
|
|||||||
@@ -5,18 +5,32 @@
|
|||||||
<p class="text-slate-400">Welcome to atlasOS Storage Controller</p>
|
<p class="text-slate-400">Welcome to atlasOS Storage Controller</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<!-- Storage Overview Card -->
|
<!-- Storage Pools Card -->
|
||||||
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-lg font-semibold text-white">Storage</h2>
|
<h2 class="text-lg font-semibold text-white">Pools</h2>
|
||||||
<div class="h-10 w-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
<div class="h-10 w-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
||||||
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold text-white mb-1">--</p>
|
<p class="text-2xl font-bold text-white mb-1" id="pool-count">-</p>
|
||||||
|
<p class="text-sm text-slate-400">ZFS Pools</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Storage Capacity Card -->
|
||||||
|
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-lg font-semibold text-white">Capacity</h2>
|
||||||
|
<div class="h-10 w-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-2xl font-bold text-white mb-1" id="total-capacity">-</p>
|
||||||
<p class="text-sm text-slate-400">Total Capacity</p>
|
<p class="text-sm text-slate-400">Total Capacity</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,8 +44,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold text-white mb-1">--</p>
|
<p class="text-2xl font-bold text-white mb-1" id="smb-shares">-</p>
|
||||||
<p class="text-sm text-slate-400">Active Shares</p>
|
<p class="text-sm text-slate-400">SMB + NFS</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- iSCSI Targets Card -->
|
<!-- iSCSI Targets Card -->
|
||||||
@@ -44,30 +58,130 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold text-white mb-1">--</p>
|
<p class="text-2xl font-bold text-white mb-1" id="iscsi-targets">-</p>
|
||||||
<p class="text-sm text-slate-400">Active Targets</p>
|
<p class="text-sm text-slate-400">Active Targets</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System Status -->
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
<!-- System Status -->
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">System Status</h2>
|
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||||
<div class="space-y-3">
|
<h2 class="text-lg font-semibold text-white mb-4">Service Status</h2>
|
||||||
<div class="flex items-center justify-between">
|
<div class="space-y-3">
|
||||||
<span class="text-slate-300">ZFS Status</span>
|
<div class="flex items-center justify-between">
|
||||||
<span class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">Initializing</span>
|
<span class="text-slate-300">SMB/Samba</span>
|
||||||
|
<span id="smb-status" class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">NFS Server</span>
|
||||||
|
<span id="nfs-status" class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">iSCSI Target</span>
|
||||||
|
<span id="iscsi-status" class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">API Status</span>
|
||||||
|
<span class="px-3 py-1 rounded-full text-xs font-medium bg-green-900 text-green-300">Online</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
</div>
|
||||||
<span class="text-slate-300">Storage Engine</span>
|
|
||||||
<span class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">Not Configured</span>
|
<!-- Jobs Status -->
|
||||||
</div>
|
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||||
<div class="flex items-center justify-between">
|
<h2 class="text-lg font-semibold text-white mb-4">Jobs</h2>
|
||||||
<span class="text-slate-300">API Status</span>
|
<div class="space-y-3">
|
||||||
<span class="px-3 py-1 rounded-full text-xs font-medium bg-green-900 text-green-300">Online</span>
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">Running</span>
|
||||||
|
<span id="jobs-running" class="text-white font-semibold">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">Completed</span>
|
||||||
|
<span id="jobs-completed" class="text-white font-semibold">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">Failed</span>
|
||||||
|
<span id="jobs-failed" class="text-white font-semibold">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300">Total</span>
|
||||||
|
<span id="jobs-total" class="text-white font-semibold">-</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||||
|
<h2 class="text-lg font-semibold text-white mb-4">Recent Activity</h2>
|
||||||
|
<div id="recent-logs" class="space-y-2">
|
||||||
|
<p class="text-slate-400 text-sm">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Fetch dashboard data and update UI
|
||||||
|
function updateDashboard() {
|
||||||
|
fetch('/api/v1/dashboard')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update storage stats
|
||||||
|
document.getElementById('pool-count').textContent = data.storage.pool_count || 0;
|
||||||
|
document.getElementById('total-capacity').textContent = formatBytes(data.storage.total_capacity || 0);
|
||||||
|
document.getElementById('smb-shares').textContent = (data.services.smb_shares || 0) + ' / ' + (data.services.nfs_exports || 0);
|
||||||
|
document.getElementById('iscsi-targets').textContent = data.services.iscsi_targets || 0;
|
||||||
|
|
||||||
|
// Update service status
|
||||||
|
updateStatus('smb-status', data.services.smb_status);
|
||||||
|
updateStatus('nfs-status', data.services.nfs_status);
|
||||||
|
updateStatus('iscsi-status', data.services.iscsi_status);
|
||||||
|
|
||||||
|
// Update jobs
|
||||||
|
document.getElementById('jobs-running').textContent = data.jobs.running || 0;
|
||||||
|
document.getElementById('jobs-completed').textContent = data.jobs.completed || 0;
|
||||||
|
document.getElementById('jobs-failed').textContent = data.jobs.failed || 0;
|
||||||
|
document.getElementById('jobs-total').textContent = data.jobs.total || 0;
|
||||||
|
|
||||||
|
// Update recent logs
|
||||||
|
const logsDiv = document.getElementById('recent-logs');
|
||||||
|
if (data.recent_audit_logs && data.recent_audit_logs.length > 0) {
|
||||||
|
logsDiv.innerHTML = data.recent_audit_logs.map(log => `
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-slate-300">${log.action} ${log.resource}</span>
|
||||||
|
<span class="px-2 py-1 rounded text-xs ${log.result === 'success' ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'}">${log.result}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
logsDiv.innerHTML = '<p class="text-slate-400 text-sm">No recent activity</p>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Dashboard update error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(id, status) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (status) {
|
||||||
|
el.className = 'px-3 py-1 rounded-full text-xs font-medium bg-green-900 text-green-300';
|
||||||
|
el.textContent = 'Running';
|
||||||
|
} else {
|
||||||
|
el.className = 'px-3 py-1 rounded-full text-xs font-medium bg-red-900 text-red-300';
|
||||||
|
el.textContent = 'Stopped';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load and periodic updates
|
||||||
|
updateDashboard();
|
||||||
|
setInterval(updateDashboard, 30000); // Update every 30 seconds
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "dashboard.html"}}
|
{{define "dashboard.html"}}
|
||||||
|
|||||||
Reference in New Issue
Block a user