feat(admin): make dashboard live and disk-aware
Wire dashboard panels to real alerts, activity, boxes, and users data instead of static mock rows. Enable working dashboard actions (close alerts, close low alerts, cleanup expired boxes, exports, and navigation). Update storage overview to use real filesystem free/total space from the uploads volume. Make top alert chip data-driven across admin pages.
This commit is contained in:
@@ -4,17 +4,93 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"warpbox/lib/activity"
|
||||
"warpbox/lib/alerts"
|
||||
"warpbox/lib/config"
|
||||
"warpbox/lib/helpers"
|
||||
"warpbox/lib/security"
|
||||
"warpbox/lib/userstore"
|
||||
)
|
||||
|
||||
const adminSessionCookie = "warpbox_admin_session"
|
||||
const adminSessionMarker = "1"
|
||||
|
||||
type adminDashboardView struct {
|
||||
ActiveBoxes int `json:"active_boxes"`
|
||||
BoxesCreatedToday int `json:"boxes_created_today"`
|
||||
PasswordedBoxes int `json:"passworded_boxes"`
|
||||
StorageUsedLabel string `json:"storage_used_label"`
|
||||
StorageFreeLabel string `json:"storage_free_label"`
|
||||
StorageCapLabel string `json:"storage_cap_label"`
|
||||
StorageMeter string `json:"storage_meter"`
|
||||
StorageBackend string `json:"storage_backend"`
|
||||
OpenAlerts int `json:"open_alerts"`
|
||||
HighAlerts int `json:"high_alerts"`
|
||||
MediumAlerts int `json:"medium_alerts"`
|
||||
LowAlerts int `json:"low_alerts"`
|
||||
TotalUsers int `json:"total_users"`
|
||||
ActiveUsers int `json:"active_users"`
|
||||
DisabledUsers int `json:"disabled_users"`
|
||||
APIKeyCount int `json:"api_key_count"`
|
||||
GuestUploadsLabel string `json:"guest_uploads_label"`
|
||||
APIUploadsLabel string `json:"api_uploads_label"`
|
||||
ZipDownloadsLabel string `json:"zip_downloads_label"`
|
||||
Alerts []adminDashboardAlert `json:"alerts"`
|
||||
Events []adminDashboardActivity `json:"events"`
|
||||
Boxes []adminDashboardBox `json:"boxes"`
|
||||
}
|
||||
|
||||
type adminDashboardAlert struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Severity string `json:"severity"`
|
||||
Status string `json:"status"`
|
||||
Group string `json:"group"`
|
||||
Code string `json:"code"`
|
||||
Trace string `json:"trace"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAtLabel string `json:"created_at_label"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type adminDashboardActivity struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
IP string `json:"ip"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAtLabel string `json:"created_at_label"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
TagClass string `json:"tag_class"`
|
||||
TagLabel string `json:"tag_label"`
|
||||
}
|
||||
|
||||
type adminDashboardBox struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
StatusLabel string `json:"status_label"`
|
||||
StatusClass string `json:"status_class"`
|
||||
FileCount int `json:"file_count"`
|
||||
CompleteFiles int `json:"complete_files"`
|
||||
TotalSizeLabel string `json:"total_size_label"`
|
||||
CreatedAtLabel string `json:"created_at_label"`
|
||||
ExpiresAtLabel string `json:"expires_at_label"`
|
||||
PasswordProtected bool `json:"password_protected"`
|
||||
OneTimeDownload bool `json:"one_time_download"`
|
||||
OpenURL string `json:"open_url"`
|
||||
ZipURL string `json:"zip_url"`
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
func (app *App) adminLoginEnabled() bool {
|
||||
return app.config.AdminLoginEnabled(app.config.AdminPassword != "")
|
||||
}
|
||||
@@ -119,14 +195,256 @@ func (app *App) handleAdminDashboard(ctx *gin.Context) {
|
||||
dashboardEnabled = cfgVal
|
||||
}
|
||||
|
||||
dashboard := app.buildAdminDashboardView()
|
||||
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
||||
"AdminUsername": app.config.AdminUsername,
|
||||
"AdminEmail": app.config.AdminEmail,
|
||||
"ActivePage": "dashboard",
|
||||
"DashboardEnabled": string(dashboardEnabled),
|
||||
"Dashboard": dashboard,
|
||||
"AlertChipClass": adminAlertChipClass(dashboard.OpenAlerts, dashboard.HighAlerts, dashboard.MediumAlerts),
|
||||
"AlertChipLabel": adminAlertChipLabel(dashboard.OpenAlerts),
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) buildAdminDashboardView() adminDashboardView {
|
||||
boxes, _ := app.listAdminBoxes()
|
||||
alertsList := []alerts.Alert{}
|
||||
if app.alertStore != nil {
|
||||
alertsList, _ = app.alertStore.List(500)
|
||||
}
|
||||
events := []activity.Event{}
|
||||
if app.activityStore != nil {
|
||||
events, _ = app.activityStore.List(80, app.config.ActivityRetentionSeconds)
|
||||
}
|
||||
users := []userstore.User{}
|
||||
if app.userStore != nil {
|
||||
users = app.userStore.List()
|
||||
}
|
||||
|
||||
view := adminDashboardView{
|
||||
StorageBackend: "local",
|
||||
GuestUploadsLabel: adminBoolLabel(app.config.GuestUploadsEnabled && app.config.APIEnabled),
|
||||
APIUploadsLabel: adminBoolLabel(app.config.APIEnabled),
|
||||
ZipDownloadsLabel: adminBoolLabel(app.config.ZipDownloadsEnabled),
|
||||
Alerts: make([]adminDashboardAlert, 0, 12),
|
||||
Events: make([]adminDashboardActivity, 0, 15),
|
||||
Boxes: make([]adminDashboardBox, 0, 12),
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
usedBytes := int64(0)
|
||||
for _, box := range boxes {
|
||||
usedBytes += box.TotalSizeBytes
|
||||
createdAt, _ := time.Parse(time.RFC3339, box.CreatedAtISO)
|
||||
if !createdAt.IsZero() && createdAt.After(dayStart) {
|
||||
view.BoxesCreatedToday++
|
||||
}
|
||||
if box.PasswordProtected {
|
||||
view.PasswordedBoxes++
|
||||
}
|
||||
if box.Status != "expired" && box.Status != "consumed" {
|
||||
view.ActiveBoxes++
|
||||
}
|
||||
if len(view.Boxes) < 12 {
|
||||
view.Boxes = append(view.Boxes, adminDashboardBox{
|
||||
ID: box.ID,
|
||||
Status: box.Status,
|
||||
StatusLabel: box.StatusLabel,
|
||||
StatusClass: adminDashboardStatusClass(box.Status),
|
||||
FileCount: box.FileCount,
|
||||
CompleteFiles: box.CompleteFiles,
|
||||
TotalSizeLabel: box.TotalSizeLabel,
|
||||
CreatedAtLabel: box.CreatedAtLabel,
|
||||
ExpiresAtLabel: box.ExpiresAtLabel,
|
||||
PasswordProtected: box.PasswordProtected,
|
||||
OneTimeDownload: box.OneTimeDownload,
|
||||
OpenURL: box.OpenURL,
|
||||
ZipURL: box.ZipURL,
|
||||
Flags: box.Flags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view.StorageUsedLabel = helpers.FormatBytes(usedBytes)
|
||||
view.StorageCapLabel = "unknown"
|
||||
view.StorageFreeLabel = "unknown"
|
||||
view.StorageMeter = "0%"
|
||||
if diskTotal, diskFree, ok := adminDiskCapacity(app.config.UploadsDir); ok && diskTotal > 0 {
|
||||
diskUsed := diskTotal - diskFree
|
||||
if diskUsed < 0 {
|
||||
diskUsed = 0
|
||||
}
|
||||
view.StorageUsedLabel = helpers.FormatBytes(diskUsed)
|
||||
view.StorageFreeLabel = helpers.FormatBytes(diskFree)
|
||||
view.StorageCapLabel = helpers.FormatBytes(diskTotal)
|
||||
percent := float64(diskUsed) / float64(diskTotal) * 100
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
view.StorageMeter = strconv.FormatFloat(percent, 'f', 1, 64) + "%"
|
||||
}
|
||||
|
||||
for _, alert := range alertsList {
|
||||
if alert.Status != alerts.StatusClosed {
|
||||
view.OpenAlerts++
|
||||
switch alert.Severity {
|
||||
case "high":
|
||||
view.HighAlerts++
|
||||
case "medium":
|
||||
view.MediumAlerts++
|
||||
default:
|
||||
view.LowAlerts++
|
||||
}
|
||||
if len(view.Alerts) < 12 {
|
||||
view.Alerts = append(view.Alerts, adminDashboardAlert{
|
||||
ID: alert.ID,
|
||||
Title: alert.Title,
|
||||
Severity: adminFallback(alert.Severity, "low"),
|
||||
Status: adminFallback(string(alert.Status), "open"),
|
||||
Group: alert.Group,
|
||||
Code: alert.Code,
|
||||
Trace: alert.Trace,
|
||||
Message: alert.Message,
|
||||
CreatedAt: formatBrowserTime(alert.CreatedAt),
|
||||
CreatedAtLabel: adminShortTimeLabel(alert.CreatedAt),
|
||||
Meta: alert.Meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if len(view.Events) >= 15 {
|
||||
break
|
||||
}
|
||||
view.Events = append(view.Events, adminDashboardActivity{
|
||||
ID: event.ID,
|
||||
Kind: event.Kind,
|
||||
Severity: adminFallback(event.Severity, "low"),
|
||||
Message: event.Message,
|
||||
IP: event.IP,
|
||||
Path: event.Path,
|
||||
Method: event.Method,
|
||||
CreatedAt: formatBrowserTime(event.CreatedAt),
|
||||
CreatedAtLabel: adminShortTimeLabel(event.CreatedAt),
|
||||
Meta: event.Meta,
|
||||
TagClass: adminSeverityTagClass(event.Severity),
|
||||
TagLabel: adminActivityTagLabel(event.Kind),
|
||||
})
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
view.TotalUsers++
|
||||
if user.Status == userstore.StatusDisabled {
|
||||
view.DisabledUsers++
|
||||
} else {
|
||||
view.ActiveUsers++
|
||||
}
|
||||
for _, key := range user.APIKeys {
|
||||
if key.RevokedAt == nil {
|
||||
view.APIKeyCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func adminBoolLabel(enabled bool) string {
|
||||
if enabled {
|
||||
return "enabled"
|
||||
}
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func adminFallback(value string, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func adminShortTimeLabel(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
return value.UTC().Format("15:04")
|
||||
}
|
||||
|
||||
func adminDashboardStatusClass(status string) string {
|
||||
switch status {
|
||||
case "ready":
|
||||
return "ok"
|
||||
case "uploading", "legacy":
|
||||
return "warn"
|
||||
case "attention", "expired", "consumed":
|
||||
return "danger"
|
||||
default:
|
||||
return "info"
|
||||
}
|
||||
}
|
||||
|
||||
func adminSeverityTagClass(severity string) string {
|
||||
switch severity {
|
||||
case "high":
|
||||
return "danger"
|
||||
case "medium":
|
||||
return "warn"
|
||||
case "low":
|
||||
return "ok"
|
||||
default:
|
||||
return "info"
|
||||
}
|
||||
}
|
||||
|
||||
func adminActivityTagLabel(kind string) string {
|
||||
parts := strings.Split(kind, ".")
|
||||
if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" {
|
||||
return "event"
|
||||
}
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
func adminDiskCapacity(path string) (int64, int64, bool) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
var stats syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stats); err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
blockSize := int64(stats.Bsize)
|
||||
if blockSize <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
total := int64(stats.Blocks) * blockSize
|
||||
free := int64(stats.Bavail) * blockSize
|
||||
return total, free, true
|
||||
}
|
||||
|
||||
func adminAlertChipClass(total int, high int, medium int) string {
|
||||
score := high*5 + medium*2 + (total - high - medium)
|
||||
switch {
|
||||
case high > 0 || score >= 12:
|
||||
return "is-danger"
|
||||
case medium >= 2 || score >= 5:
|
||||
return "is-warning"
|
||||
case total > 0:
|
||||
return "is-info"
|
||||
default:
|
||||
return "is-ok"
|
||||
}
|
||||
}
|
||||
|
||||
func adminAlertChipLabel(total int) string {
|
||||
if total == 0 {
|
||||
return "OK no alerts"
|
||||
}
|
||||
return "! " + strconv.Itoa(total) + " alerts"
|
||||
}
|
||||
|
||||
func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
||||
if !app.adminLoginEnabled() {
|
||||
ctx.Redirect(http.StatusSeeOther, "/")
|
||||
@@ -144,6 +462,7 @@ func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
||||
}
|
||||
openCount := 0
|
||||
highCount := 0
|
||||
mediumCount := 0
|
||||
ackedCount := 0
|
||||
closedCount := 0
|
||||
for _, alert := range alertsList {
|
||||
@@ -158,16 +477,21 @@ func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
||||
if alert.Severity == "high" && string(alert.Status) != "closed" {
|
||||
highCount++
|
||||
}
|
||||
if alert.Severity == "medium" && string(alert.Status) != "closed" {
|
||||
mediumCount++
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
||||
"AdminUsername": app.config.AdminUsername,
|
||||
"AdminEmail": app.config.AdminEmail,
|
||||
"ActivePage": "alerts",
|
||||
"Alerts": alertsList,
|
||||
"OpenCount": strconv.Itoa(openCount),
|
||||
"HighCount": strconv.Itoa(highCount),
|
||||
"AckCount": strconv.Itoa(ackedCount),
|
||||
"ClosedCount": strconv.Itoa(closedCount),
|
||||
"AdminUsername": app.config.AdminUsername,
|
||||
"AdminEmail": app.config.AdminEmail,
|
||||
"ActivePage": "alerts",
|
||||
"Alerts": alertsList,
|
||||
"OpenCount": strconv.Itoa(openCount),
|
||||
"HighCount": strconv.Itoa(highCount),
|
||||
"AckCount": strconv.Itoa(ackedCount),
|
||||
"ClosedCount": strconv.Itoa(closedCount),
|
||||
"AlertChipClass": adminAlertChipClass(openCount, highCount, mediumCount),
|
||||
"AlertChipLabel": adminAlertChipLabel(openCount),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user