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"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"warpbox/lib/activity"
|
||||||
"warpbox/lib/alerts"
|
"warpbox/lib/alerts"
|
||||||
"warpbox/lib/config"
|
"warpbox/lib/config"
|
||||||
|
"warpbox/lib/helpers"
|
||||||
"warpbox/lib/security"
|
"warpbox/lib/security"
|
||||||
|
"warpbox/lib/userstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
const adminSessionCookie = "warpbox_admin_session"
|
const adminSessionCookie = "warpbox_admin_session"
|
||||||
const adminSessionMarker = "1"
|
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 {
|
func (app *App) adminLoginEnabled() bool {
|
||||||
return app.config.AdminLoginEnabled(app.config.AdminPassword != "")
|
return app.config.AdminLoginEnabled(app.config.AdminPassword != "")
|
||||||
}
|
}
|
||||||
@@ -119,14 +195,256 @@ func (app *App) handleAdminDashboard(ctx *gin.Context) {
|
|||||||
dashboardEnabled = cfgVal
|
dashboardEnabled = cfgVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dashboard := app.buildAdminDashboardView()
|
||||||
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
||||||
"AdminUsername": app.config.AdminUsername,
|
"AdminUsername": app.config.AdminUsername,
|
||||||
"AdminEmail": app.config.AdminEmail,
|
"AdminEmail": app.config.AdminEmail,
|
||||||
"ActivePage": "dashboard",
|
"ActivePage": "dashboard",
|
||||||
"DashboardEnabled": string(dashboardEnabled),
|
"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) {
|
func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
||||||
if !app.adminLoginEnabled() {
|
if !app.adminLoginEnabled() {
|
||||||
ctx.Redirect(http.StatusSeeOther, "/")
|
ctx.Redirect(http.StatusSeeOther, "/")
|
||||||
@@ -144,6 +462,7 @@ func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
openCount := 0
|
openCount := 0
|
||||||
highCount := 0
|
highCount := 0
|
||||||
|
mediumCount := 0
|
||||||
ackedCount := 0
|
ackedCount := 0
|
||||||
closedCount := 0
|
closedCount := 0
|
||||||
for _, alert := range alertsList {
|
for _, alert := range alertsList {
|
||||||
@@ -158,16 +477,21 @@ func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
|||||||
if alert.Severity == "high" && string(alert.Status) != "closed" {
|
if alert.Severity == "high" && string(alert.Status) != "closed" {
|
||||||
highCount++
|
highCount++
|
||||||
}
|
}
|
||||||
|
if alert.Severity == "medium" && string(alert.Status) != "closed" {
|
||||||
|
mediumCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
||||||
"AdminUsername": app.config.AdminUsername,
|
"AdminUsername": app.config.AdminUsername,
|
||||||
"AdminEmail": app.config.AdminEmail,
|
"AdminEmail": app.config.AdminEmail,
|
||||||
"ActivePage": "alerts",
|
"ActivePage": "alerts",
|
||||||
"Alerts": alertsList,
|
"Alerts": alertsList,
|
||||||
"OpenCount": strconv.Itoa(openCount),
|
"OpenCount": strconv.Itoa(openCount),
|
||||||
"HighCount": strconv.Itoa(highCount),
|
"HighCount": strconv.Itoa(highCount),
|
||||||
"AckCount": strconv.Itoa(ackedCount),
|
"AckCount": strconv.Itoa(ackedCount),
|
||||||
"ClosedCount": strconv.Itoa(closedCount),
|
"ClosedCount": strconv.Itoa(closedCount),
|
||||||
|
"AlertChipClass": adminAlertChipClass(openCount, highCount, mediumCount),
|
||||||
|
"AlertChipLabel": adminAlertChipLabel(openCount),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type adminBoxView struct {
|
|||||||
CompleteFiles int `json:"complete_files"`
|
CompleteFiles int `json:"complete_files"`
|
||||||
PendingFiles int `json:"pending_files"`
|
PendingFiles int `json:"pending_files"`
|
||||||
FailedFiles int `json:"failed_files"`
|
FailedFiles int `json:"failed_files"`
|
||||||
|
TotalSizeBytes int64 `json:"total_size_bytes"`
|
||||||
TotalSizeLabel string `json:"total_size_label"`
|
TotalSizeLabel string `json:"total_size_label"`
|
||||||
CreatedAtLabel string `json:"created_at_label"`
|
CreatedAtLabel string `json:"created_at_label"`
|
||||||
CreatedAtISO string `json:"created_at_iso"`
|
CreatedAtISO string `json:"created_at_iso"`
|
||||||
@@ -203,6 +204,7 @@ func (app *App) buildAdminBoxView(boxID string) (adminBoxView, error) {
|
|||||||
boxView := adminBoxView{
|
boxView := adminBoxView{
|
||||||
ID: summary.ID,
|
ID: summary.ID,
|
||||||
FileCount: summary.FileCount,
|
FileCount: summary.FileCount,
|
||||||
|
TotalSizeBytes: summary.TotalSize,
|
||||||
TotalSizeLabel: summary.TotalSizeLabel,
|
TotalSizeLabel: summary.TotalSizeLabel,
|
||||||
CreatedAtLabel: adminTimeLabel(summary.CreatedAt),
|
CreatedAtLabel: adminTimeLabel(summary.CreatedAt),
|
||||||
CreatedAtISO: formatBrowserTime(summary.CreatedAt),
|
CreatedAtISO: formatBrowserTime(summary.CreatedAt),
|
||||||
|
|||||||
@@ -111,6 +111,18 @@
|
|||||||
.alerts-scroll { height: 326px; }
|
.alerts-scroll { height: 326px; }
|
||||||
.boxes-scroll { height: 352px; }
|
.boxes-scroll { height: 352px; }
|
||||||
.activity-scroll { height: 326px; }
|
.activity-scroll { height: 326px; }
|
||||||
|
.dashboard-empty-state {
|
||||||
|
margin: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #333333;
|
||||||
|
background: #ffffcc;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #a08000;
|
||||||
|
border-bottom: 1px solid #a08000;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Alerts */
|
/* Alerts */
|
||||||
.alert-list { display: grid; min-width: 0; }
|
.alert-list { display: grid; min-width: 0; }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const dataNode = document.getElementById("dashboard-data");
|
||||||
const toast = document.getElementById("toast");
|
const toast = document.getElementById("toast");
|
||||||
const statusText = document.getElementById("statusText");
|
const statusText = document.getElementById("statusText");
|
||||||
const modal = document.querySelector("[data-alert-modal]");
|
const modal = document.querySelector("[data-alert-modal]");
|
||||||
@@ -19,18 +20,28 @@
|
|||||||
const topAlertChip = document.getElementById("topAlertChip");
|
const topAlertChip = document.getElementById("topAlertChip");
|
||||||
const topTaskbar = document.querySelector(".admin-taskbar");
|
const topTaskbar = document.querySelector(".admin-taskbar");
|
||||||
|
|
||||||
|
const dashboardData = parseDashboardData();
|
||||||
|
|
||||||
if (!statusText || !alertsCard || !topAlertChip) return;
|
if (!statusText || !alertsCard || !topAlertChip) return;
|
||||||
|
|
||||||
function showToast(message, type = "info") {
|
function parseDashboardData() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(dataNode?.textContent || "{}");
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = "info", duration = 2200) {
|
||||||
if (window.WarpBoxUI) {
|
if (window.WarpBoxUI) {
|
||||||
window.WarpBoxUI.toast(message, type, { target: toast });
|
window.WarpBoxUI.toast(message, type, { target: toast, duration });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!toast) return;
|
if (!toast) return;
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
toast.classList.add("is-visible");
|
toast.classList.add("is-visible");
|
||||||
window.clearTimeout(showToast.timer);
|
window.clearTimeout(showToast.timer);
|
||||||
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), 2600);
|
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(message) {
|
function setStatus(message) {
|
||||||
@@ -91,45 +102,159 @@
|
|||||||
setStatus(`Focused ${id.replace("-", " ")}`);
|
setStatus(`Focused ${id.replace("-", " ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandMessages = {
|
function downloadFile(filename, content, type) {
|
||||||
refresh: "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard refresh would re-fetch dashboard data.",
|
const blob = new Blob([content], { type });
|
||||||
"dashboard-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard snapshot export would start here.",
|
const url = URL.createObjectURL(blob);
|
||||||
logout: "CURRENTLY_MOCKED_LEAVE_AS_IS: logout would submit to the account logout route.",
|
const anchor = document.createElement("a");
|
||||||
"compact-mode": "Toggled compact density.",
|
anchor.href = url;
|
||||||
"show-all-boxes": "TO-DO: navigate to the admin boxes view when that page exists.",
|
anchor.download = filename;
|
||||||
"show-all-alerts": "TO-DO: navigate to /admin/alerts.",
|
anchor.click();
|
||||||
"export-boxes": "CURRENTLY_MOCKED_LEAVE_AS_IS: boxes CSV export would be requested.",
|
URL.revokeObjectURL(url);
|
||||||
"export-alerts": "CURRENTLY_MOCKED_LEAVE_AS_IS: alerts JSON export would be requested.",
|
}
|
||||||
"cleanup-dry-run": "CURRENTLY_MOCKED_LEAVE_AS_IS: cleanup dry run would calculate affected boxes without deleting.",
|
|
||||||
"dismiss-low-alerts": "Closed visible low-severity alerts in this mock.",
|
|
||||||
"config-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: config snapshot would summarize runtime settings and sources.",
|
|
||||||
"support-summary": "CURRENTLY_MOCKED_LEAVE_AS_IS: support summary would collect safe diagnostic information.",
|
|
||||||
"thumbnail-rebuild": "CURRENTLY_MOCKED_LEAVE_AS_IS: thumbnail rebuild would enqueue preview regeneration.",
|
|
||||||
"open-users": "TO-DO: navigate to the admin users view when that page exists.",
|
|
||||||
"open-settings": "TO-DO: navigate to the admin settings view when that page exists.",
|
|
||||||
"alerts-help": "Alerts use title, description, severity, metadata JSON, trace identifier, and unique numeric code.",
|
|
||||||
shortcuts: "Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.",
|
|
||||||
about: "WarpBox dashboard mock v5, single-window Win98 account dashboard."
|
|
||||||
};
|
|
||||||
|
|
||||||
function runCommand(command) {
|
function csvEscape(value) {
|
||||||
if (command === "compact-mode") document.body.classList.toggle("is-compact");
|
const text = String(value ?? "");
|
||||||
if (command === "dismiss-low-alerts") {
|
if (!/[",\n]/.test(text)) return text;
|
||||||
document.querySelectorAll('.alert-row[data-severity="low"]').forEach((row) => row.classList.add("is-dismissed"));
|
return `"${text.replaceAll('"', '""')}"`;
|
||||||
updateAlertSummary();
|
}
|
||||||
|
|
||||||
|
function exportBoxesCSV() {
|
||||||
|
const rows = dashboardData.boxes || [];
|
||||||
|
const header = ["id", "status", "files", "size", "created", "expires", "flags"];
|
||||||
|
const lines = rows.map((box) => [
|
||||||
|
box.id,
|
||||||
|
box.status_label,
|
||||||
|
`${box.complete_files}/${box.file_count}`,
|
||||||
|
box.total_size_label,
|
||||||
|
box.created_at_label,
|
||||||
|
box.expires_at_label,
|
||||||
|
(box.flags || []).join("|")
|
||||||
|
].map(csvEscape).join(","));
|
||||||
|
downloadFile(`warpbox-dashboard-boxes-${new Date().toISOString().replaceAll(":", "-")}.csv`, [header.join(","), ...lines].join("\n"), "text/csv;charset=utf-8");
|
||||||
|
showToast("Dashboard boxes exported", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAlertsJSON() {
|
||||||
|
downloadFile(`warpbox-dashboard-alerts-${new Date().toISOString().replaceAll(":", "-")}.json`, JSON.stringify(dashboardData.alerts || [], null, 2), "application/json;charset=utf-8");
|
||||||
|
showToast("Dashboard alerts exported", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSnapshot() {
|
||||||
|
downloadFile(`warpbox-dashboard-${new Date().toISOString().replaceAll(":", "-")}.json`, JSON.stringify(dashboardData, null, 2), "application/json;charset=utf-8");
|
||||||
|
showToast("Dashboard snapshot exported", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postAlertAction(action, ids) {
|
||||||
|
const response = await fetch("/admin/alerts/actions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action, ids })
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.error || "Alert action failed");
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postBoxAction(action, extra = {}) {
|
||||||
|
const response = await fetch("/admin/boxes/actions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action, ...extra })
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.error || "Box action failed");
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeAlert(row) {
|
||||||
|
const id = row?.dataset.alertId;
|
||||||
|
if (!id) return;
|
||||||
|
await postAlertAction("close", [id]);
|
||||||
|
row.classList.add("is-dismissed");
|
||||||
|
updateAlertSummary();
|
||||||
|
showToast(`Closed alert ${row.dataset.alertCode || id}`, "success");
|
||||||
|
setStatus(`Closed alert ${row.dataset.alertCode || id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeLowAlerts() {
|
||||||
|
const rows = Array.from(document.querySelectorAll('.alert-row[data-severity="low"]'));
|
||||||
|
const ids = rows.map((row) => row.dataset.alertId).filter(Boolean);
|
||||||
|
if (!ids.length) {
|
||||||
|
showToast("No low alerts to close");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (command === "show-all-boxes") window.location.hash = "recent-boxes";
|
await postAlertAction("close", ids);
|
||||||
if (command === "show-all-alerts") window.location.hash = "alerts";
|
rows.forEach((row) => row.classList.add("is-dismissed"));
|
||||||
|
updateAlertSummary();
|
||||||
|
showToast(`Closed ${ids.length} low alert(s)`, "success");
|
||||||
|
setStatus(`Closed ${ids.length} low alert(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
const message = commandMessages[command] || `Command: ${command}`;
|
async function cleanupExpiredBoxes() {
|
||||||
showToast(message);
|
if (!window.confirm("Clean up expired boxes now? This can delete expired box data.")) return;
|
||||||
setStatus(message);
|
const payload = await postBoxAction("cleanup_expired");
|
||||||
|
showToast(payload.message || "Expired cleanup complete", payload.ok ? "success" : "warning", 3200);
|
||||||
|
setStatus(payload.message || "Expired cleanup complete");
|
||||||
|
window.setTimeout(() => window.location.reload(), 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(command) {
|
||||||
|
if (command === "refresh") {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "dashboard-snapshot") return exportSnapshot();
|
||||||
|
if (command === "logout") {
|
||||||
|
window.location.href = "/admin/logout";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "compact-mode") {
|
||||||
|
document.body.classList.toggle("is-compact");
|
||||||
|
showToast("Toggled compact density");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "show-all-boxes") {
|
||||||
|
window.location.href = "/admin/boxes";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "show-all-alerts") {
|
||||||
|
window.location.href = "/admin/alerts";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "open-users") {
|
||||||
|
window.location.href = "/admin/users";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "open-activity") {
|
||||||
|
window.location.href = "/admin/activity";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "open-settings") {
|
||||||
|
window.location.href = "/admin/settings";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "export-boxes") return exportBoxesCSV();
|
||||||
|
if (command === "export-alerts") return exportAlertsJSON();
|
||||||
|
if (command === "close-low-alerts") return closeLowAlerts();
|
||||||
|
if (command === "cleanup-expired") return cleanupExpiredBoxes();
|
||||||
|
if (command === "shortcuts") {
|
||||||
|
showToast("Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.", "info", 3600);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "about") {
|
||||||
|
showToast("Live WarpBox admin dashboard backed by alerts, activity, boxes, users, and settings.", "info", 3600);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-command]").forEach((button) => {
|
document.querySelectorAll("[data-command]").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", async () => {
|
||||||
menuController.close();
|
menuController.close();
|
||||||
runCommand(button.dataset.command);
|
try {
|
||||||
|
await runCommand(button.dataset.command);
|
||||||
|
} catch (error) {
|
||||||
|
showToast(error.message || "Command failed", "error", 3600);
|
||||||
|
setStatus(error.message || "Command failed");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,37 +275,39 @@
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
meta = row?.dataset.alertMeta || "{}";
|
meta = row?.dataset.alertMeta || "{}";
|
||||||
}
|
}
|
||||||
openModal(`${title} (${row?.dataset.alertCode || "mock"})`, meta);
|
openModal(`${title} (${row?.dataset.alertCode || "alert"})`, meta);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll("[data-dismiss-alert]").forEach((button) => {
|
document.querySelectorAll("[data-close-alert]").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", async () => {
|
||||||
const row = button.closest(".alert-row");
|
try {
|
||||||
row?.classList.add("is-dismissed");
|
await closeAlert(button.closest(".alert-row"));
|
||||||
updateAlertSummary();
|
} catch (error) {
|
||||||
showToast(`Closed alert ${row?.dataset.alertCode || "mock"}.`);
|
showToast(error.message || "Could not close alert", "error", 3600);
|
||||||
setStatus(`Closed alert ${row?.dataset.alertCode || "mock"}`);
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("[data-close-modal]")?.addEventListener("click", closeModal);
|
document.querySelector("[data-close-modal]")?.addEventListener("click", closeModal);
|
||||||
backdrop?.addEventListener("click", closeModal);
|
backdrop?.addEventListener("click", closeModal);
|
||||||
topAlertChip.addEventListener("click", (event) => {
|
topAlertChip.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
if (document.getElementById("alerts")) {
|
||||||
scrollToSection("alerts");
|
event.preventDefault();
|
||||||
|
scrollToSection("alerts");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("scroll", updateStickyHeader, { passive: true });
|
window.addEventListener("scroll", updateStickyHeader, { passive: true });
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", async (event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
menuController.close();
|
menuController.close();
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
if (event.key === "F5") {
|
if (event.key === "F5") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
runCommand("refresh");
|
await runCommand("refresh");
|
||||||
}
|
}
|
||||||
if (event.altKey && event.key.toLowerCase() === "a") {
|
if (event.altKey && event.key.toLowerCase() === "a") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -14,13 +14,12 @@
|
|||||||
<link rel="stylesheet" href="/static/css/admin.css">
|
<link rel="stylesheet" href="/static/css/admin.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{{ $d := .Dashboard }}
|
||||||
<div class="admin-shell">
|
<div class="admin-shell">
|
||||||
<div class="admin-frame">
|
<div class="admin-frame">
|
||||||
{{ template "admin/header.html" . }}
|
{{ template "admin/header.html" . }}
|
||||||
|
|
||||||
<!-- Dashboard Window -->
|
|
||||||
<div class="win98-window admin-dashboard-window" role="main">
|
<div class="win98-window admin-dashboard-window" role="main">
|
||||||
<!-- Titlebar -->
|
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Menu Bar -->
|
|
||||||
<nav class="menu-bar" aria-label="Dashboard toolbar">
|
<nav class="menu-bar" aria-label="Dashboard toolbar">
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
||||||
@@ -57,83 +55,74 @@
|
|||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false">Boxes</button>
|
<button class="menu-button" type="button" aria-expanded="false">Boxes</button>
|
||||||
<div class="menu-popup">
|
<div class="menu-popup">
|
||||||
<button class="menu-action" type="button" data-command="show-all-boxes"><span>B</span><span>Show all boxes</span><span></span></button>
|
<button class="menu-action" type="button" data-command="show-all-boxes"><span>B</span><span>Open boxes page</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="export-boxes"><span>C</span><span>Export boxes CSV</span><span></span></button>
|
<button class="menu-action" type="button" data-command="export-boxes"><span>C</span><span>Export visible boxes CSV</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="cleanup-dry-run"><span>D</span><span>Cleanup dry run</span><span></span></button>
|
<button class="menu-action" type="button" data-command="cleanup-expired"><span>D</span><span>Cleanup expired boxes</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false">Alerts</button>
|
<button class="menu-button" type="button" aria-expanded="false">Alerts</button>
|
||||||
<div class="menu-popup">
|
<div class="menu-popup">
|
||||||
<button class="menu-action" type="button" data-command="show-all-alerts"><span>!</span><span>Show all alerts</span><span></span></button>
|
<button class="menu-action" type="button" data-command="show-all-alerts"><span>!</span><span>Open alerts page</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="dismiss-low-alerts"><span>L</span><span>Close all low alerts</span><span></span></button>
|
<button class="menu-action" type="button" data-command="close-low-alerts"><span>L</span><span>Close low alerts</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="export-alerts"><span>J</span><span>Export alerts JSON</span><span></span></button>
|
<button class="menu-action" type="button" data-command="export-alerts"><span>J</span><span>Export visible alerts JSON</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false">Admin</button>
|
<button class="menu-button" type="button" aria-expanded="false">Admin</button>
|
||||||
<div class="menu-popup">
|
<div class="menu-popup">
|
||||||
<button class="menu-action" type="button" data-command="config-snapshot"><span>S</span><span>Config snapshot</span><span></span></button>
|
|
||||||
<button class="menu-action" type="button" data-command="support-summary"><span>?</span><span>Support summary</span><span></span></button>
|
|
||||||
<button class="menu-action" type="button" data-command="thumbnail-rebuild"><span>I</span><span>Queue thumbnail rebuild</span><span></span></button>
|
|
||||||
<div class="menu-separator"></div>
|
|
||||||
<button class="menu-action" type="button" data-command="open-users"><span>U</span><span>Open user manager</span><span></span></button>
|
<button class="menu-action" type="button" data-command="open-users"><span>U</span><span>Open user manager</span><span></span></button>
|
||||||
|
<button class="menu-action" type="button" data-command="open-activity"><span>A</span><span>Open activity log</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="open-settings"><span>G</span><span>Open settings</span><span></span></button>
|
<button class="menu-action" type="button" data-command="open-settings"><span>G</span><span>Open settings</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false">Help</button>
|
<button class="menu-button" type="button" aria-expanded="false">Help</button>
|
||||||
<div class="menu-popup">
|
<div class="menu-popup">
|
||||||
<button class="menu-action" type="button" data-command="alerts-help"><span>!</span><span>How alert tracing works</span><span></span></button>
|
|
||||||
<button class="menu-action" type="button" data-command="shortcuts"><span>K</span><span>Keyboard shortcuts</span><span></span></button>
|
<button class="menu-action" type="button" data-command="shortcuts"><span>K</span><span>Keyboard shortcuts</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-command="about"><span>W</span><span>About this mockup</span><span></span></button>
|
<button class="menu-action" type="button" data-command="about"><span>W</span><span>About this dashboard</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Dashboard Body -->
|
|
||||||
<div class="dashboard-body">
|
<div class="dashboard-body">
|
||||||
<!-- Hero -->
|
|
||||||
<section class="dashboard-hero raised-panel" aria-labelledby="dashboardTitle">
|
<section class="dashboard-hero raised-panel" aria-labelledby="dashboardTitle">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<h2 id="dashboardTitle">Dashboard</h2>
|
<h2 id="dashboardTitle">Dashboard</h2>
|
||||||
<p>At-a-glance account and admin overview for boxes, alerts, storage, users, and recent activity.</p>
|
<p>Live overview for boxes, alerts, storage, users, and recent account activity.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-status" aria-label="System summary">
|
<div class="hero-status" aria-label="System summary">
|
||||||
<div class="hero-status-row"><span>Guest uploads</span><strong class="status-ok">enabled</strong></div>
|
<div class="hero-status-row"><span>Guest uploads</span><strong class="{{ if eq $d.GuestUploadsLabel "enabled" }}status-ok{{ else }}status-danger{{ end }}">{{ $d.GuestUploadsLabel }}</strong></div>
|
||||||
<div class="hero-status-row"><span>ZIP downloads</span><strong class="status-ok">enabled</strong></div>
|
<div class="hero-status-row"><span>API uploads</span><strong class="{{ if eq $d.APIUploadsLabel "enabled" }}status-ok{{ else }}status-danger{{ end }}">{{ $d.APIUploadsLabel }}</strong></div>
|
||||||
<div class="hero-status-row"><span>One-time boxes</span><strong class="status-warn">limited</strong></div>
|
<div class="hero-status-row"><span>ZIP downloads</span><strong class="{{ if eq $d.ZipDownloadsLabel "enabled" }}status-ok{{ else }}status-warn{{ end }}">{{ $d.ZipDownloadsLabel }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<section class="stats-grid" aria-label="Dashboard statistics">
|
<section class="stats-grid" aria-label="Dashboard statistics">
|
||||||
<article class="stat-card sunken-panel is-info" id="activeBoxesCard">
|
<article class="stat-card sunken-panel is-info" id="activeBoxesCard">
|
||||||
<p class="stat-label">Active boxes</p>
|
<p class="stat-label">Active boxes</p>
|
||||||
<p class="stat-value">128</p>
|
<p class="stat-value">{{ $d.ActiveBoxes }}</p>
|
||||||
<p class="stat-note"><span class="stat-note-pill">+12 today</span><span class="stat-note-pill">42 passworded</span></p>
|
<p class="stat-note"><span class="stat-note-pill">+{{ $d.BoxesCreatedToday }} today</span><span class="stat-note-pill">{{ $d.PasswordedBoxes }} passworded</span></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat-card sunken-panel is-info" id="storageCard">
|
<article class="stat-card sunken-panel is-info" id="storageCard">
|
||||||
<p class="stat-label">Storage available</p>
|
<p class="stat-label">Storage available</p>
|
||||||
<p class="stat-value">812 GiB</p>
|
<p class="stat-value">{{ $d.StorageFreeLabel }}</p>
|
||||||
<p class="stat-note"><span class="stat-note-pill">188 GiB used</span><span class="stat-note-pill">1 TiB app cap</span><span class="stat-note-pill">local backend</span></p>
|
<p class="stat-note"><span class="stat-note-pill">{{ $d.StorageUsedLabel }} used</span><span class="stat-note-pill">{{ $d.StorageCapLabel }} cap</span><span class="stat-note-pill">{{ $d.StorageBackend }} backend</span></p>
|
||||||
<span class="meter-track" aria-hidden="true"><span class="meter-bar" style="--meter: 18.8%"></span></span>
|
<span class="meter-track" aria-hidden="true"><span class="meter-bar" style="--meter: {{ $d.StorageMeter }}"></span></span>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat-card sunken-panel is-warning" id="alertsCard">
|
<article class="stat-card sunken-panel is-warning" id="alertsCard">
|
||||||
<p class="stat-label">Alerts</p>
|
<p class="stat-label">Alerts</p>
|
||||||
<p class="stat-value"><span id="alertCountValue">15</span></p>
|
<p class="stat-value"><span id="alertCountValue">{{ $d.OpenAlerts }}</span></p>
|
||||||
<p class="stat-note" id="alertStatNote"><span class="stat-note-pill">2 high</span><span class="stat-note-pill">5 medium</span><span class="stat-note-pill">8 low</span></p>
|
<p class="stat-note" id="alertStatNote"><span class="stat-note-pill">{{ $d.HighAlerts }} high</span><span class="stat-note-pill">{{ $d.MediumAlerts }} medium</span><span class="stat-note-pill">{{ $d.LowAlerts }} low</span></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat-card sunken-panel is-ok" id="usersCard">
|
<article class="stat-card sunken-panel is-ok" id="usersCard">
|
||||||
<p class="stat-label">Users</p>
|
<p class="stat-label">Users</p>
|
||||||
<p class="stat-value">19</p>
|
<p class="stat-value">{{ $d.TotalUsers }}</p>
|
||||||
<p class="stat-note"><span class="stat-note-pill">15 active</span><span class="stat-note-pill">4 disabled</span><span class="stat-note-pill">admin-only</span></p>
|
<p class="stat-note"><span class="stat-note-pill">{{ $d.ActiveUsers }} active</span><span class="stat-note-pill">{{ $d.DisabledUsers }} disabled</span><span class="stat-note-pill">{{ $d.APIKeyCount }} API keys</span></p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Main Grid: Alerts, Boxes, Activity -->
|
|
||||||
<section class="dashboard-main-grid" aria-label="Dashboard panels">
|
<section class="dashboard-main-grid" aria-label="Dashboard panels">
|
||||||
<!-- Alerts -->
|
|
||||||
<article id="alerts" class="win98-window section-window">
|
<article id="alerts" class="win98-window section-window">
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
@@ -147,87 +136,29 @@
|
|||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
<div class="scroll-panel alerts-scroll" aria-label="Scrollable alerts inbox">
|
<div class="scroll-panel alerts-scroll" aria-label="Scrollable alerts inbox">
|
||||||
<div class="alert-list">
|
<div class="alert-list">
|
||||||
<div class="alert-row" data-severity="high" data-alert-title="Storage backend is almost full" data-alert-code="421" data-alert-meta='{"backend":"local","used_bytes":1009317314560,"available_bytes":45097156608,"configured_cap_bytes":1099511627776,"recommended_action":"run cleanup dry run or raise app cap"}'>
|
{{ if $d.Alerts }}
|
||||||
<span class="alert-severity">high</span>
|
{{ range $d.Alerts }}
|
||||||
<div><p class="alert-title">Storage backend is almost full</p><p class="alert-desc">The active local storage backend has less than 5% free capacity under the configured app cap.</p><p class="alert-trace">code 421, trace storage.local.capacity.high</p></div>
|
<div class="alert-row" data-alert-id="{{ .ID }}" data-severity="{{ .Severity }}" data-alert-title="{{ .Title }}" data-alert-code="{{ .Code }}" data-alert-meta='{{ toJSON .Meta }}'>
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
<span class="alert-severity">{{ .Severity }}</span>
|
||||||
</div>
|
<div>
|
||||||
<div class="alert-row" data-severity="high" data-alert-title="Disabled user has active sessions" data-alert-code="181" data-alert-meta='{"user":"old-operator","active_sessions":2,"recommended_action":"revoke sessions"}'>
|
<p class="alert-title">{{ .Title }}</p>
|
||||||
<span class="alert-severity">high</span>
|
<p class="alert-desc">{{ .Message }}</p>
|
||||||
<div><p class="alert-title">Disabled user has active sessions</p><p class="alert-desc">A disabled account still has active sessions that should be revoked.</p><p class="alert-trace">code 181, trace auth.sessions.disabled_user_active</p></div>
|
<p class="alert-trace">{{ .Code }} {{ .Trace }} · {{ .CreatedAtLabel }} UTC · {{ .Status }}</p>
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
</div>
|
||||||
</div>
|
<div class="alert-actions">
|
||||||
<div class="alert-row" data-severity="medium" data-alert-title="Expired boxes waiting cleanup" data-alert-code="301" data-alert-meta='{"expired_boxes":17,"oldest_expired_at":"2026-04-29T22:18:00+03:00","recommended_action":"run cleanup"}'>
|
<button class="tiny-button" type="button" data-view-meta>Meta</button>
|
||||||
<span class="alert-severity">medium</span>
|
<button class="tiny-button" type="button" data-close-alert>Close</button>
|
||||||
<div><p class="alert-title">Expired boxes waiting cleanup</p><p class="alert-desc">Expired boxes are still present on disk and are eligible for cleanup.</p><p class="alert-trace">code 301, trace boxes.expiry.cleanup_pending</p></div>
|
</div>
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
</div>
|
||||||
</div>
|
{{ end }}
|
||||||
<div class="alert-row" data-severity="medium" data-alert-title="API key UI enabled but key backend missing" data-alert-code="711" data-alert-meta='{"ui_surface":"upload.api_key_input","backend_model":"missing","recommended_action":"hide UI or implement API keys"}'>
|
{{ else }}
|
||||||
<span class="alert-severity">medium</span>
|
<div class="dashboard-empty-state">No open alerts. Nice and boring, which is the good kind of admin dashboard.</div>
|
||||||
<div><p class="alert-title">API key UI enabled but key backend missing</p><p class="alert-desc">The frontend advertises API key usage while server-side API key validation is not connected yet.</p><p class="alert-trace">code 711, trace api_keys.ui.backend_missing</p></div>
|
{{ end }}
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="medium" data-alert-title="Thumbnail queue is behind" data-alert-code="602" data-alert-meta='{"pending_thumbnails":44,"worker_interval_seconds":30,"recommended_action":"increase batch size or queue rebuild"}'>
|
|
||||||
<span class="alert-severity">medium</span>
|
|
||||||
<div><p class="alert-title">Thumbnail queue is behind</p><p class="alert-desc">The thumbnail worker has accumulated more pending previews than expected.</p><p class="alert-trace">code 602, trace thumbnails.worker.queue_lag</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="medium" data-alert-title="Large ZIP download failed" data-alert-code="502" data-alert-meta='{"box":"BX-7D20","zip_bytes":897300992,"attempt":1,"recommended_action":"retry manually or inspect files"}'>
|
|
||||||
<span class="alert-severity">medium</span>
|
|
||||||
<div><p class="alert-title">Large ZIP download failed</p><p class="alert-desc">A ZIP stream failed before the response finished.</p><p class="alert-trace">code 502, trace downloads.zip.stream_failed</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="medium" data-alert-title="Guest quota close to daily cap" data-alert-code="231" data-alert-meta='{"ip":"192.0.2.44","used_today_bytes":1795162112,"daily_cap_bytes":2147483648,"recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">medium</span>
|
|
||||||
<div><p class="alert-title">Guest quota close to daily cap</p><p class="alert-desc">A guest IP is close to its configured daily upload cap.</p><p class="alert-trace">code 231, trace quotas.guest.daily.near_cap</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Thumbnail generation skipped" data-alert-code="601" data-alert-meta='{"box":"BX-9F31","file":"mockup.webp","reason":"unsupported decoder","recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Thumbnail generation skipped</p><p class="alert-desc">A preview could not be generated for one image file.</p><p class="alert-trace">code 601, trace thumbnails.generate.skipped</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="One-time box downloaded" data-alert-code="511" data-alert-meta='{"box":"BX-440C","delete_after_success":true,"recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">One-time box downloaded</p><p class="alert-desc">A one-time ZIP handoff completed and the box was queued for deletion.</p><p class="alert-trace">code 511, trace downloads.one_time.completed</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Settings override changed" data-alert-code="801" data-alert-meta='{"setting":"box_poll_interval_ms","source":"admin_override","recommended_action":"audit when audit log exists"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Settings override changed</p><p class="alert-desc">A runtime setting was changed through the settings UI.</p><p class="alert-trace">code 801, trace settings.override.changed</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Password protected box created" data-alert-code="121" data-alert-meta='{"box":"BX-C2A8","owner":"maya","recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Password protected box created</p><p class="alert-desc">A user created a password protected upload box.</p><p class="alert-trace">code 121, trace boxes.create.passworded</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Upload completed slowly" data-alert-code="222" data-alert-meta='{"box":"BX-88B4","duration_seconds":731,"recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Upload completed slowly</p><p class="alert-desc">An upload completed but exceeded the expected duration threshold.</p><p class="alert-trace">code 222, trace uploads.performance.slow_complete</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Session refreshed" data-alert-code="182" data-alert-meta='{"user":"admin","reason":"activity_refresh","recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Session refreshed</p><p class="alert-desc">The current local session was refreshed after account activity.</p><p class="alert-trace">code 182, trace auth.session.refreshed</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Box visited from share URL" data-alert-code="401" data-alert-meta='{"box":"BX-39C1","viewer":"guest","recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Box visited from share URL</p><p class="alert-desc">A public box was opened through its normal shared page.</p><p class="alert-trace">code 401, trace boxes.share.opened</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="alert-row" data-severity="low" data-alert-title="Support summary generated" data-alert-code="901" data-alert-meta='{"requested_by":"admin","included_sections":["config","storage","alerts"],"recommended_action":"none"}'>
|
|
||||||
<span class="alert-severity">low</span>
|
|
||||||
<div><p class="alert-title">Support summary generated</p><p class="alert-desc">A local support summary was generated from the toolbar.</p><p class="alert-trace">code 901, trace support.summary.generated</p></div>
|
|
||||||
<div class="alert-actions"><button class="tiny-button" type="button" data-view-meta>Meta</button><button class="tiny-button" type="button" data-dismiss-alert>Close</button></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<!-- Recent Activity -->
|
|
||||||
<article id="recent-activity" class="win98-window section-window">
|
<article id="recent-activity" class="win98-window section-window">
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
@@ -235,33 +166,31 @@
|
|||||||
<h2>Recent Activity</h2>
|
<h2>Recent Activity</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
<a class="titlebar-link-button" href="/admin/dashboard#recent-activity">Show all</a>
|
<a class="titlebar-link-button" href="/admin/activity">Show all</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
<div class="scroll-panel activity-scroll" aria-label="Scrollable recent activity list">
|
<div class="scroll-panel activity-scroll" aria-label="Scrollable recent activity list">
|
||||||
<div class="activity-list">
|
<div class="activity-list">
|
||||||
<div class="activity-row"><span class="activity-time">10:12</span><div><p class="activity-title">Box BX-9F31 completed upload</p><p class="activity-meta">4 files, password protected</p></div><span class="tag ok">box</span></div>
|
{{ if $d.Events }}
|
||||||
<div class="activity-row"><span class="activity-time">10:08</span><div><p class="activity-title">Alert 421 created</p><p class="activity-meta">storage.local.capacity.high</p></div><span class="tag danger">alert</span></div>
|
{{ range $d.Events }}
|
||||||
<div class="activity-row"><span class="activity-time">10:04</span><div><p class="activity-title">Guest created box BX-A71D</p><p class="activity-meta">retention 6 hours</p></div><span class="tag ok">upload</span></div>
|
<div class="activity-row">
|
||||||
<div class="activity-row"><span class="activity-time">09:58</span><div><p class="activity-title">Thumbnail worker skipped one image</p><p class="activity-meta">decoder unavailable for webp preview</p></div><span class="tag warn">thumbs</span></div>
|
<span class="activity-time">{{ .CreatedAtLabel }}</span>
|
||||||
<div class="activity-row"><span class="activity-time">09:51</span><div><p class="activity-title">Cleanup dry run opened</p><p class="activity-meta">17 expired boxes detected</p></div><span class="tag info">tools</span></div>
|
<div>
|
||||||
<div class="activity-row"><span class="activity-time">09:44</span><div><p class="activity-title">Large ZIP download completed</p><p class="activity-meta">BX-7D20, 12 files</p></div><span class="tag info">zip</span></div>
|
<p class="activity-title">{{ .Message }}</p>
|
||||||
<div class="activity-row"><span class="activity-time">09:33</span><div><p class="activity-title">Settings snapshot requested</p><p class="activity-meta">admin opened config snapshot from toolbar</p></div><span class="tag info">settings</span></div>
|
<p class="activity-meta">{{ .Kind }} · {{ .Method }} {{ .Path }} {{ if .IP }}· {{ .IP }}{{ end }}</p>
|
||||||
<div class="activity-row"><span class="activity-time">09:21</span><div><p class="activity-title">Temporary cleanup skipped</p><p class="activity-meta">BX-1AA2 still had an active file handle</p></div><span class="tag warn">cleanup</span></div>
|
</div>
|
||||||
<div class="activity-row"><span class="activity-time">09:09</span><div><p class="activity-title">User maya uploaded 6 files</p><p class="activity-meta">91.9 MiB total</p></div><span class="tag ok">user</span></div>
|
<span class="tag {{ .TagClass }}">{{ .TagLabel }}</span>
|
||||||
<div class="activity-row"><span class="activity-time">08:55</span><div><p class="activity-title">Box BX-55E0 expired</p><p class="activity-meta">eligible for cleanup</p></div><span class="tag danger">expired</span></div>
|
</div>
|
||||||
<div class="activity-row"><span class="activity-time">08:42</span><div><p class="activity-title">One-time box created</p><p class="activity-meta">BX-440C, admin owner</p></div><span class="tag info">one-time</span></div>
|
{{ end }}
|
||||||
<div class="activity-row"><span class="activity-time">08:31</span><div><p class="activity-title">User ana uploaded archive set</p><p class="activity-meta">7 files, 520.8 MiB</p></div><span class="tag ok">upload</span></div>
|
{{ else }}
|
||||||
<div class="activity-row"><span class="activity-time">08:20</span><div><p class="activity-title">Guest accessed public box</p><p class="activity-meta">BX-39C1 viewed from share link</p></div><span class="tag info">access</span></div>
|
<div class="dashboard-empty-state">No activity has been recorded yet.</div>
|
||||||
<div class="activity-row"><span class="activity-time">08:07</span><div><p class="activity-title">User mihai created box BX-F02A</p><p class="activity-meta">standard plan quota applied</p></div><span class="tag ok">quota</span></div>
|
{{ end }}
|
||||||
<div class="activity-row"><span class="activity-time">07:54</span><div><p class="activity-title">Failed login attempt recorded</p><p class="activity-meta">admin account, single attempt</p></div><span class="tag warn">auth</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<!-- Recent Boxes (full width) -->
|
|
||||||
<article id="recent-boxes" class="win98-window section-window dashboard-span-2">
|
<article id="recent-boxes" class="win98-window section-window dashboard-span-2">
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
@@ -269,29 +198,33 @@
|
|||||||
<h2>Recent Boxes</h2>
|
<h2>Recent Boxes</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
<a class="titlebar-link-button" href="/admin/dashboard#recent-boxes">Show all</a>
|
<a class="titlebar-link-button" href="/admin/boxes">Show all</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
<div class="scroll-panel boxes-scroll" aria-label="Scrollable recent boxes table">
|
<div class="scroll-panel boxes-scroll" aria-label="Scrollable recent boxes table">
|
||||||
<table class="box-table">
|
<table class="box-table">
|
||||||
<thead><tr><th>Box</th><th>Owner</th><th>Files</th><th>Size</th><th>Created</th><th>Expires</th><th>Flags</th><th>Actions</th></tr></thead>
|
<thead><tr><th>Box</th><th>Status</th><th>Files</th><th>Size</th><th>Created</th><th>Expires</th><th>Flags</th><th>Actions</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>BX-9F31</td><td>maya</td><td>4</td><td>91.9 MiB</td><td>10:12</td><td>5h 41m</td><td><span class="tag ok">complete</span> <span class="tag info">password</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-9F31">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-9F31">Manage</a></div></td></tr>
|
{{ if $d.Boxes }}
|
||||||
<tr><td>BX-A71D</td><td>guest</td><td>12</td><td>1.8 GiB</td><td>10:04</td><td>6h 00m</td><td><span class="tag warn">large</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-A71D">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-A71D">Manage</a></div></td></tr>
|
{{ range $d.Boxes }}
|
||||||
<tr><td>BX-20BD</td><td>operator</td><td>2</td><td>8.4 MiB</td><td>09:58</td><td>1d 12h</td><td><span class="tag ok">complete</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-20BD">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-20BD">Manage</a></div></td></tr>
|
<tr data-box-id="{{ .ID }}">
|
||||||
<tr><td>BX-7D20</td><td>admin</td><td>12</td><td>856.3 MiB</td><td>09:44</td><td>23h 11m</td><td><span class="tag danger">zip failed</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-7D20">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-7D20">Manage</a></div></td></tr>
|
<td>{{ .ID }}</td>
|
||||||
<tr><td>BX-1AA2</td><td>guest</td><td>1</td><td>4.7 GiB</td><td>09:21</td><td>expired</td><td><span class="tag danger">locked</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-1AA2">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-1AA2">Manage</a></div></td></tr>
|
<td><span class="tag {{ .StatusClass }}">{{ .StatusLabel }}</span></td>
|
||||||
<tr><td>BX-C2A8</td><td>maya</td><td>6</td><td>24.8 MiB</td><td>09:09</td><td>2d 03h</td><td><span class="tag ok">complete</span> <span class="tag info">password</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-C2A8">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-C2A8">Manage</a></div></td></tr>
|
<td>{{ .CompleteFiles }}/{{ .FileCount }}</td>
|
||||||
<tr><td>BX-55E0</td><td>guest</td><td>1</td><td>4.2 MiB</td><td>08:55</td><td>expired</td><td><span class="tag danger">expired</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-55E0">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-55E0">Manage</a></div></td></tr>
|
<td>{{ .TotalSizeLabel }}</td>
|
||||||
<tr><td>BX-440C</td><td>admin</td><td>3</td><td>63.0 MiB</td><td>08:42</td><td>2d 00h</td><td><span class="tag ok">complete</span> <span class="tag info">one-time</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-440C">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-440C">Manage</a></div></td></tr>
|
<td>{{ .CreatedAtLabel }}</td>
|
||||||
<tr><td>BX-88B4</td><td>ana</td><td>7</td><td>520.8 MiB</td><td>08:31</td><td>5d 00h</td><td><span class="tag ok">complete</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-88B4">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-88B4">Manage</a></div></td></tr>
|
<td>{{ .ExpiresAtLabel }}</td>
|
||||||
<tr><td>BX-39C1</td><td>guest</td><td>2</td><td>23.1 MiB</td><td>08:20</td><td>16h 00m</td><td><span class="tag info">public</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-39C1">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-39C1">Manage</a></div></td></tr>
|
<td>
|
||||||
<tr><td>BX-F02A</td><td>mihai</td><td>5</td><td>108.6 MiB</td><td>08:07</td><td>4d 00h</td><td><span class="tag ok">complete</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-F02A">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-F02A">Manage</a></div></td></tr>
|
{{ range .Flags }}<span class="tag info">{{ . }}</span>{{ end }}
|
||||||
<tr><td>BX-ABC4</td><td>guest</td><td>1</td><td>755 KiB</td><td>07:54</td><td>3h 00m</td><td><span class="tag ok">complete</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-ABC4">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-ABC4">Manage</a></div></td></tr>
|
{{ if not .Flags }}<span class="tag ok">plain</span>{{ end }}
|
||||||
<tr><td>BX-74E9</td><td>operator</td><td>10</td><td>987.3 MiB</td><td>07:41</td><td>7d 00h</td><td><span class="tag info">bulk</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-74E9">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-74E9">Manage</a></div></td></tr>
|
</td>
|
||||||
<tr><td>BX-218B</td><td>daniel</td><td>3</td><td>44.0 MiB</td><td>07:28</td><td>1d 00h</td><td><span class="tag ok">complete</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-218B">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-218B">Manage</a></div></td></tr>
|
<td><div class="box-actions"><a class="win98-button box-action-button" href="{{ .OpenURL }}">Open</a><a class="win98-button box-action-button" href="/admin/boxes?q={{ .ID }}">Manage</a></div></td>
|
||||||
<tr><td>BX-00FE</td><td>guest</td><td>2</td><td>13.7 MiB</td><td>07:12</td><td>2h 00m</td><td><span class="tag warn">soon</span></td><td><div class="box-actions"><a class="win98-button box-action-button" href="/box/BX-00FE">Open</a><a class="win98-button box-action-button" href="/account/boxes/BX-00FE">Manage</a></div></td></tr>
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<tr><td colspan="8">No boxes found yet.</td></tr>
|
||||||
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,20 +233,17 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statusbar -->
|
|
||||||
<div class="win98-statusbar admin-dashboard-statusbar">
|
<div class="win98-statusbar admin-dashboard-statusbar">
|
||||||
<span id="statusText">Ready</span>
|
<span id="statusText">Ready</span>
|
||||||
<span>WarpBox mock v5</span>
|
<span>{{ $d.OpenAlerts }} open alert(s)</span>
|
||||||
<span>Single-window dashboard</span>
|
<span>Live dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal backdrop -->
|
|
||||||
<div class="modal-backdrop" data-modal-backdrop></div>
|
<div class="modal-backdrop" data-modal-backdrop></div>
|
||||||
|
|
||||||
<!-- Alert metadata popup -->
|
|
||||||
<aside class="popup-window win98-window" data-alert-modal aria-label="Alert metadata" aria-hidden="true">
|
<aside class="popup-window win98-window" data-alert-modal aria-label="Alert metadata" aria-hidden="true">
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
@@ -327,9 +257,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script id="dashboard-data" type="application/json">{{ toJSON $d }}</script>
|
||||||
<script src="/static/js/warpbox-ui.js"></script>
|
<script src="/static/js/warpbox-ui.js"></script>
|
||||||
<script src="/static/js/admin/dashboard.js"></script>
|
<script src="/static/js/admin/dashboard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<a class="admin-taskbar-button{{ if eq .ActivePage "settings" }} is-active{{ end }}" href="/admin/settings">Settings</a>
|
<a class="admin-taskbar-button{{ if eq .ActivePage "settings" }} is-active{{ end }}" href="/admin/settings">Settings</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="admin-taskbar-session" aria-label="Admin session summary">
|
<div class="admin-taskbar-session" aria-label="Admin session summary">
|
||||||
<a class="admin-alert-chip is-warning" href="/admin/alerts" id="topAlertChip">! 15 alerts</a>
|
<a class="admin-alert-chip {{ with .AlertChipClass }}{{ . }}{{ else }}is-info{{ end }}" href="/admin/alerts" id="topAlertChip">{{ with .AlertChipLabel }}{{ . }}{{ else }}Alerts{{ end }}</a>
|
||||||
<span class="admin-session-chip">signed in: {{ .AdminUsername }}</span>
|
<span class="admin-session-chip">signed in: {{ .AdminUsername }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user