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.
340 lines
9.8 KiB
Go
340 lines
9.8 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/boxstore"
|
|
)
|
|
|
|
type adminBoxesActionRequest struct {
|
|
Action string `json:"action"`
|
|
BoxIDs []string `json:"box_ids"`
|
|
DeltaSeconds int64 `json:"delta_seconds,omitempty"`
|
|
}
|
|
|
|
type adminBoxFileView struct {
|
|
Name string `json:"name"`
|
|
SizeLabel string `json:"size_label"`
|
|
MimeType string `json:"mime_type"`
|
|
Status string `json:"status"`
|
|
StatusLabel string `json:"status_label"`
|
|
DownloadPath string `json:"download_path"`
|
|
ThumbnailURL string `json:"thumbnail_url"`
|
|
IsComplete bool `json:"is_complete"`
|
|
}
|
|
|
|
type adminBoxView struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
StatusLabel string `json:"status_label"`
|
|
FileCount int `json:"file_count"`
|
|
CompleteFiles int `json:"complete_files"`
|
|
PendingFiles int `json:"pending_files"`
|
|
FailedFiles int `json:"failed_files"`
|
|
TotalSizeBytes int64 `json:"total_size_bytes"`
|
|
TotalSizeLabel string `json:"total_size_label"`
|
|
CreatedAtLabel string `json:"created_at_label"`
|
|
CreatedAtISO string `json:"created_at_iso"`
|
|
ExpiresAtLabel string `json:"expires_at_label"`
|
|
ExpiresAtISO string `json:"expires_at_iso"`
|
|
RetentionLabel string `json:"retention_label"`
|
|
PasswordProtected bool `json:"password_protected"`
|
|
OneTimeDownload bool `json:"one_time_download"`
|
|
ZipDisabled bool `json:"zip_disabled"`
|
|
ZipAvailable bool `json:"zip_available"`
|
|
Consumed bool `json:"consumed"`
|
|
HasManifest bool `json:"has_manifest"`
|
|
OpenURL string `json:"open_url"`
|
|
ZipURL string `json:"zip_url"`
|
|
Flags []string `json:"flags"`
|
|
Files []adminBoxFileView `json:"files"`
|
|
SearchText string `json:"search_text"`
|
|
}
|
|
|
|
func (app *App) handleAdminBoxes(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
return
|
|
}
|
|
|
|
boxes, err := app.listAdminBoxes()
|
|
if err != nil {
|
|
ctx.String(http.StatusInternalServerError, "Could not load boxes")
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, "admin/boxes.html", gin.H{
|
|
"AdminUsername": app.config.AdminUsername,
|
|
"AdminEmail": app.config.AdminEmail,
|
|
"ActivePage": "boxes",
|
|
"Boxes": boxes,
|
|
"ZipDownloadsOn": app.config.ZipDownloadsEnabled,
|
|
})
|
|
}
|
|
|
|
func (app *App) handleAdminBoxesAction(ctx *gin.Context) {
|
|
var request adminBoxesActionRequest
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action payload"})
|
|
return
|
|
}
|
|
|
|
switch request.Action {
|
|
case "delete", "expire", "bump", "cleanup_expired":
|
|
default:
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Unknown action"})
|
|
return
|
|
}
|
|
|
|
if request.Action != "cleanup_expired" && len(request.BoxIDs) == 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Select one or more boxes first"})
|
|
return
|
|
}
|
|
|
|
if request.Action == "bump" && request.DeltaSeconds <= 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing bump duration"})
|
|
return
|
|
}
|
|
if request.Action == "cleanup_expired" {
|
|
result, err := app.runExpiredCleanup("admin")
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Expired cleanup job failed"})
|
|
return
|
|
}
|
|
boxes, listErr := app.listAdminBoxes()
|
|
if listErr != nil {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Cleanup finished, but boxes could not be reloaded"})
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"ok": len(result.Warnings) == 0,
|
|
"message": fmt.Sprintf("Expired cleanup done: deleted %d box(es), skipped %d", result.Deleted, result.Skipped),
|
|
"warnings": result.Warnings,
|
|
"boxes": boxes,
|
|
})
|
|
return
|
|
}
|
|
|
|
processed := 0
|
|
warnings := make([]string, 0)
|
|
|
|
for _, boxID := range request.BoxIDs {
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
warnings = append(warnings, fmt.Sprintf("%s: invalid box id", boxID))
|
|
continue
|
|
}
|
|
|
|
var err error
|
|
switch request.Action {
|
|
case "delete":
|
|
err = boxstore.DeleteBox(boxID)
|
|
case "expire":
|
|
_, err = boxstore.ExpireBox(boxID)
|
|
case "bump":
|
|
_, err = boxstore.BumpBoxExpiry(boxID, time.Duration(request.DeltaSeconds)*time.Second)
|
|
}
|
|
|
|
if err != nil {
|
|
warnings = append(warnings, fmt.Sprintf("%s: %v", boxID, err))
|
|
continue
|
|
}
|
|
processed++
|
|
}
|
|
|
|
boxes, err := app.listAdminBoxes()
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Action finished, but boxes could not be reloaded"})
|
|
return
|
|
}
|
|
|
|
status := http.StatusOK
|
|
if processed == 0 && len(warnings) > 0 {
|
|
status = http.StatusBadRequest
|
|
}
|
|
|
|
ctx.JSON(status, gin.H{
|
|
"ok": len(warnings) == 0,
|
|
"message": adminBoxesActionMessage(request.Action, processed, request.DeltaSeconds),
|
|
"warnings": warnings,
|
|
"boxes": boxes,
|
|
})
|
|
}
|
|
|
|
func (app *App) listAdminBoxes() ([]adminBoxView, error) {
|
|
summaries, err := boxstore.ListBoxSummaries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
boxes := make([]adminBoxView, 0, len(summaries))
|
|
for _, summary := range summaries {
|
|
boxView, err := app.buildAdminBoxView(summary.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
boxes = append(boxes, boxView)
|
|
}
|
|
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
return boxes[i].CreatedAtISO > boxes[j].CreatedAtISO
|
|
})
|
|
return boxes, nil
|
|
}
|
|
|
|
func (app *App) buildAdminBoxView(boxID string) (adminBoxView, error) {
|
|
summary, err := boxstore.BoxSummary(boxID)
|
|
if err != nil {
|
|
return adminBoxView{}, err
|
|
}
|
|
|
|
files, err := boxstore.ListFiles(boxID)
|
|
if err != nil {
|
|
return adminBoxView{}, err
|
|
}
|
|
|
|
manifest, manifestErr := boxstore.ReadManifest(boxID)
|
|
hasManifest := manifestErr == nil
|
|
|
|
boxView := adminBoxView{
|
|
ID: summary.ID,
|
|
FileCount: summary.FileCount,
|
|
TotalSizeBytes: summary.TotalSize,
|
|
TotalSizeLabel: summary.TotalSizeLabel,
|
|
CreatedAtLabel: adminTimeLabel(summary.CreatedAt),
|
|
CreatedAtISO: formatBrowserTime(summary.CreatedAt),
|
|
ExpiresAtLabel: "Not set",
|
|
ExpiresAtISO: formatBrowserTime(summary.ExpiresAt),
|
|
RetentionLabel: "Legacy / unmanaged",
|
|
PasswordProtected: summary.PasswordProtected,
|
|
OneTimeDownload: summary.OneTimeDownload,
|
|
HasManifest: hasManifest,
|
|
OpenURL: "/box/" + summary.ID,
|
|
Files: make([]adminBoxFileView, 0, len(files)),
|
|
}
|
|
|
|
if !summary.ExpiresAt.IsZero() {
|
|
boxView.ExpiresAtLabel = adminTimeLabel(summary.ExpiresAt)
|
|
}
|
|
|
|
searchParts := []string{summary.ID, summary.TotalSizeLabel}
|
|
for _, file := range files {
|
|
if file.IsComplete {
|
|
boxView.CompleteFiles++
|
|
}
|
|
if file.Status == "failed" {
|
|
boxView.FailedFiles++
|
|
}
|
|
if !file.IsComplete && file.Status != "failed" {
|
|
boxView.PendingFiles++
|
|
}
|
|
|
|
boxView.Files = append(boxView.Files, adminBoxFileView{
|
|
Name: file.Name,
|
|
SizeLabel: file.SizeLabel,
|
|
MimeType: file.MimeType,
|
|
Status: file.Status,
|
|
StatusLabel: file.StatusLabel,
|
|
DownloadPath: file.DownloadPath,
|
|
ThumbnailURL: file.ThumbnailURL,
|
|
IsComplete: file.IsComplete,
|
|
})
|
|
searchParts = append(searchParts, file.Name, file.MimeType, file.StatusLabel)
|
|
}
|
|
|
|
if hasManifest {
|
|
boxView.RetentionLabel = manifest.RetentionLabel
|
|
boxView.ZipDisabled = manifest.DisableZip
|
|
boxView.Consumed = manifest.Consumed
|
|
} else {
|
|
boxView.ZipDisabled = false
|
|
}
|
|
|
|
boxView.ZipAvailable = app.config.ZipDownloadsEnabled && !boxView.ZipDisabled && !boxView.Consumed && boxView.FileCount > 0 && boxView.PendingFiles == 0
|
|
if boxView.ZipAvailable {
|
|
boxView.ZipURL = "/box/" + summary.ID + "/download"
|
|
}
|
|
|
|
boxView.Status, boxView.StatusLabel = deriveAdminBoxStatus(hasManifest, summary.Expired, boxView.PendingFiles, boxView.FailedFiles, boxView.Consumed)
|
|
boxView.Flags = deriveAdminBoxFlags(boxView)
|
|
searchParts = append(searchParts, boxView.StatusLabel, boxView.RetentionLabel)
|
|
boxView.SearchText = strings.ToLower(strings.Join(searchParts, " "))
|
|
|
|
return boxView, nil
|
|
}
|
|
|
|
func deriveAdminBoxStatus(hasManifest bool, expired bool, pendingFiles int, failedFiles int, consumed bool) (string, string) {
|
|
switch {
|
|
case !hasManifest:
|
|
return "legacy", "Legacy"
|
|
case consumed:
|
|
return "consumed", "Consumed"
|
|
case expired:
|
|
return "expired", "Expired"
|
|
case pendingFiles > 0:
|
|
return "uploading", "Uploading"
|
|
case failedFiles > 0:
|
|
return "attention", "Needs review"
|
|
default:
|
|
return "ready", "Ready"
|
|
}
|
|
}
|
|
|
|
func deriveAdminBoxFlags(box adminBoxView) []string {
|
|
flags := make([]string, 0, 5)
|
|
if box.PasswordProtected {
|
|
flags = append(flags, "protected")
|
|
}
|
|
if box.OneTimeDownload {
|
|
flags = append(flags, "one-time")
|
|
}
|
|
if box.ZipDisabled {
|
|
flags = append(flags, "zip off")
|
|
}
|
|
if !box.HasManifest {
|
|
flags = append(flags, "legacy")
|
|
}
|
|
if box.Consumed {
|
|
flags = append(flags, "consumed")
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func adminTimeLabel(value time.Time) string {
|
|
if value.IsZero() {
|
|
return "Not set"
|
|
}
|
|
return value.UTC().Format("2006-01-02 15:04 UTC")
|
|
}
|
|
|
|
func adminBoxesActionMessage(action string, processed int, deltaSeconds int64) string {
|
|
switch action {
|
|
case "delete":
|
|
return fmt.Sprintf("Deleted %d box(es)", processed)
|
|
case "expire":
|
|
return fmt.Sprintf("Expired %d box(es)", processed)
|
|
case "bump":
|
|
return fmt.Sprintf("Extended %d box(es) by %s", processed, adminBoxesDeltaLabel(deltaSeconds))
|
|
case "cleanup_expired":
|
|
return fmt.Sprintf("Expired cleanup processed %d box(es)", processed)
|
|
default:
|
|
return "Action complete"
|
|
}
|
|
}
|
|
|
|
func adminBoxesDeltaLabel(deltaSeconds int64) string {
|
|
switch deltaSeconds {
|
|
case 24 * 60 * 60:
|
|
return "24h"
|
|
case 7 * 24 * 60 * 60:
|
|
return "7d"
|
|
default:
|
|
return (time.Duration(deltaSeconds) * time.Second).String()
|
|
}
|
|
}
|