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:
2026-05-04 10:54:44 +03:00
parent d7cbba1bf2
commit a2c80ac105
6 changed files with 600 additions and 205 deletions

View File

@@ -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),
})
}