Add storage analytics page
This commit is contained in:
@@ -66,6 +66,12 @@ func main() {
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"humanSize": util.HumanSize,
|
||||
"percent": func(used, total int64) int {
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((float64(used) / float64(total)) * 100)
|
||||
},
|
||||
})
|
||||
|
||||
r.LoadHTMLGlob("templates/*.html")
|
||||
|
||||
6
go.mod
6
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/sys v0.42.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
@@ -23,6 +24,7 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
@@ -44,15 +46,17 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
|
||||
10
go.sum
10
go.sum
@@ -18,6 +18,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -78,6 +80,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
@@ -85,6 +89,8 @@ github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRC
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -101,6 +107,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -113,6 +121,8 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -209,3 +211,66 @@ func (s *Service) buildPath(id, filename string) string {
|
||||
func (s *Service) GetAllFiles() ([]FileRecord, error) {
|
||||
return s.repo.GetAll()
|
||||
}
|
||||
|
||||
func (s *Service) GetStorageStats() (*StorageStats, error) {
|
||||
files, err := s.repo.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := &StorageStats{}
|
||||
|
||||
var total int64
|
||||
var active, deleted int
|
||||
|
||||
for _, f := range files {
|
||||
stats.TotalFiles++
|
||||
|
||||
if f.Deleted {
|
||||
deleted++
|
||||
} else {
|
||||
active++
|
||||
}
|
||||
|
||||
total += f.Size
|
||||
}
|
||||
|
||||
stats.TotalBytes = total
|
||||
stats.ActiveFiles = active
|
||||
stats.DeletedFiles = deleted
|
||||
|
||||
if stats.TotalFiles > 0 {
|
||||
stats.AverageFileSize = total / int64(stats.TotalFiles)
|
||||
}
|
||||
|
||||
// Biggest files
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Size > files[j].Size
|
||||
})
|
||||
|
||||
if len(files) > 10 {
|
||||
stats.LargestFiles = files[:10]
|
||||
} else {
|
||||
stats.LargestFiles = files
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(s.storageDir)
|
||||
if err == nil {
|
||||
stats.DiskTotalBytes = int64(usage.Total)
|
||||
stats.DiskFreeBytes = int64(usage.Free)
|
||||
stats.DiskUsedBytes = int64(usage.Used)
|
||||
}
|
||||
|
||||
// tmp chunk usage
|
||||
var tmpTotal int64
|
||||
filepath.Walk("tmp", func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() {
|
||||
tmpTotal += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
stats.TempBytes = tmpTotal
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
18
internal/file/stats.go
Normal file
18
internal/file/stats.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package file
|
||||
|
||||
type StorageStats struct {
|
||||
TotalFiles int
|
||||
ActiveFiles int
|
||||
DeletedFiles int
|
||||
|
||||
TotalBytes int64
|
||||
AverageFileSize int64
|
||||
|
||||
DiskTotalBytes int64
|
||||
DiskFreeBytes int64
|
||||
DiskUsedBytes int64
|
||||
|
||||
TempBytes int64
|
||||
|
||||
LargestFiles []FileRecord
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (h *Handler) AdminPage(c *gin.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := 10
|
||||
limit := 25
|
||||
offset := (page - 1) * limit
|
||||
|
||||
files, totalCount, err := h.fileService.GetPaginatedFiles(limit, offset)
|
||||
@@ -103,11 +103,6 @@ func (h *Handler) AdminPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only check files on the current page.
|
||||
// Status meanings:
|
||||
// - green: file exists on disk
|
||||
// - red: file missing
|
||||
// - rainbow: stat error (something unexpected)
|
||||
type AdminFileView struct {
|
||||
file.FileRecord
|
||||
ActualStatus string
|
||||
@@ -153,4 +148,26 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ChangePasswordPage(c *gin.Context) {
|
||||
c.HTML(200, "changePassword.html", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) AdminStatsPage(c *gin.Context) {
|
||||
log := middleware.StructuredLog(c).With().
|
||||
Str("event", "admin_stats_page").
|
||||
Logger()
|
||||
|
||||
stats, err := h.fileService.GetStorageStats()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load storage stats")
|
||||
|
||||
c.HTML(500, "admin_stats.html", gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("Admin stats page viewed")
|
||||
|
||||
c.HTML(200, "admin_stats.html", gin.H{
|
||||
"Stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func RegisterRoutes(r *gin.Engine, h *Handler, userService *user.Service) {
|
||||
adminRoutes.Use(user.ForcePasswordChangeMiddleware(userService))
|
||||
|
||||
adminRoutes.GET("/admin", h.AdminPage)
|
||||
adminRoutes.GET("/admin/stats", h.AdminStatsPage)
|
||||
adminRoutes.GET("/config", h.ConfigPage)
|
||||
adminRoutes.POST("/config", h.ConfigSave)
|
||||
adminRoutes.GET("/logout", h.Logout)
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<a href="/" class="nav-link">← BACK_TO_UPLOADER</a>
|
||||
<a href="/config" class="nav-link">CONFIG_MODULE</a>
|
||||
<a href="/admin/stats" class="nav-link">STORAGE_MATRIX</a>
|
||||
<a href="/logout" class="nav-link text-red-600">LOGOUT_SESSION</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
309
templates/admin_stats.html
Normal file
309
templates/admin_stats.html
Normal file
@@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Storage Matrix</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<style>
|
||||
* { border-radius: 0 !important; }
|
||||
|
||||
body {
|
||||
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
border: 4px solid #000;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 900;
|
||||
text-decoration: underline;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 2px solid #000;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #ffff00;
|
||||
}
|
||||
|
||||
.meter {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 3px solid #000;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meter-fill {
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.status-live {
|
||||
background: #00ff00;
|
||||
color: #000;
|
||||
font-weight: 900;
|
||||
padding: 4px 8px;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
.status-deleted {
|
||||
background: #ff0000;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
padding: 4px 8px;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
.glitch {
|
||||
animation: glitch 0.6s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0% { transform: translate(0px, 0px); }
|
||||
20% { transform: translate(-2px, 1px); }
|
||||
40% { transform: translate(2px, -1px); }
|
||||
60% { transform: translate(-1px, 2px); }
|
||||
80% { transform: translate(1px, -2px); }
|
||||
100% { transform: translate(0px, 0px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
<header class="mb-8 border-b-8 border-black pb-4 flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-5xl font-black uppercase tracking-tighter leading-none">
|
||||
STORAGE_ANALYTICS
|
||||
</h1>
|
||||
|
||||
<div class="text-sm font-black uppercase mt-2 text-gray-600">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<a href="/admin" class="nav-link">← BACK_TO_ADMIN</a>
|
||||
<a href="/config" class="nav-link">CONFIG_MODULE</a>
|
||||
<a href="/logout" class="nav-link text-red-600">LOGOUT_SESSION</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
<div class="box p-6">
|
||||
<div class="text-xs font-black uppercase mb-2">
|
||||
TOTAL_STORAGE_USED
|
||||
</div>
|
||||
|
||||
<div class="text-5xl font-black">
|
||||
{{humanSize .Stats.TotalBytes}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box p-6">
|
||||
<div class="text-xs font-black uppercase mb-2">
|
||||
DISK_FREE_SPACE
|
||||
</div>
|
||||
|
||||
<div class="text-5xl font-black text-green-600">
|
||||
{{humanSize .Stats.DiskFreeBytes}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box p-6">
|
||||
<div class="text-xs font-black uppercase mb-2">
|
||||
TEMP_CHUNK_STORAGE
|
||||
</div>
|
||||
|
||||
<div class="text-5xl font-black text-yellow-500">
|
||||
{{humanSize .Stats.TempBytes}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- DISK BAR -->
|
||||
<div class="box p-6 mb-8">
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="text-xl font-black uppercase">
|
||||
DISK_CONSUMPTION
|
||||
</div>
|
||||
|
||||
<div class="text-sm font-black uppercase">
|
||||
{{humanSize .Stats.DiskUsedBytes}}
|
||||
/
|
||||
{{humanSize .Stats.DiskTotalBytes}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meter">
|
||||
<div
|
||||
class="meter-fill"
|
||||
style="width: {{percent .Stats.DiskUsedBytes .Stats.DiskTotalBytes}}%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-6xl font-black">
|
||||
{{percent .Stats.DiskUsedBytes .Stats.DiskTotalBytes}}%
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SECONDARY STATS -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
|
||||
<div class="box p-4">
|
||||
<div class="text-xs uppercase font-black">
|
||||
TOTAL_FILES
|
||||
</div>
|
||||
|
||||
<div class="text-4xl font-black mt-2">
|
||||
{{.Stats.TotalFiles}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box p-4">
|
||||
<div class="text-xs uppercase font-black">
|
||||
ACTIVE_FILES
|
||||
</div>
|
||||
|
||||
<div class="text-4xl font-black text-green-600 mt-2">
|
||||
{{.Stats.ActiveFiles}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box p-4">
|
||||
<div class="text-xs uppercase font-black">
|
||||
DELETED_FILES
|
||||
</div>
|
||||
|
||||
<div class="text-4xl font-black text-red-600 mt-2">
|
||||
{{.Stats.DeletedFiles}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box p-4">
|
||||
<div class="text-xs uppercase font-black">
|
||||
AVERAGE_SIZE
|
||||
</div>
|
||||
|
||||
<div class="text-4xl font-black mt-2">
|
||||
{{humanSize .Stats.AverageFileSize}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="box overflow-x-auto mb-8">
|
||||
|
||||
<div class="bg-black text-white p-4 font-black uppercase text-lg">
|
||||
LARGEST_FILES_ON_SYSTEM
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Size</th>
|
||||
<th>Downloads</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{{if not .Stats.LargestFiles}}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-10 uppercase font-black italic">
|
||||
No files detected in storage matrix
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
{{range .Stats.LargestFiles}}
|
||||
<tr>
|
||||
|
||||
<td class="font-bold">
|
||||
{{.Filename}}
|
||||
</td>
|
||||
|
||||
<td class="whitespace-nowrap">
|
||||
{{humanSize .Size}}
|
||||
</td>
|
||||
|
||||
<td class="font-black text-lg">
|
||||
{{.DownloadCount}}
|
||||
</td>
|
||||
|
||||
<td class="text-[11px]">
|
||||
{{.CreatedAt.Format "02/01/06 15:04"}}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{if .Deleted}}
|
||||
<span class="status-deleted">
|
||||
DELETED
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="status-live">
|
||||
LIVE
|
||||
</span>
|
||||
{{end}}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="border-t-8 border-black pt-4 flex justify-between items-center">
|
||||
|
||||
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user