Add storage analytics page
This commit is contained in:
@@ -66,6 +66,12 @@ func main() {
|
|||||||
"add": func(a, b int) int { return a + b },
|
"add": func(a, b int) int { return a + b },
|
||||||
"sub": func(a, b int) int { return a - b },
|
"sub": func(a, b int) int { return a - b },
|
||||||
"humanSize": util.HumanSize,
|
"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")
|
r.LoadHTMLGlob("templates/*.html")
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
golang.org/x/crypto v0.49.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/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlite 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/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // 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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.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
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sync v0.20.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
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // 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-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 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
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=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
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=
|
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/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 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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/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 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
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 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
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/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 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -209,3 +211,66 @@ func (s *Service) buildPath(id, filename string) string {
|
|||||||
func (s *Service) GetAllFiles() ([]FileRecord, error) {
|
func (s *Service) GetAllFiles() ([]FileRecord, error) {
|
||||||
return s.repo.GetAll()
|
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
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := 10
|
limit := 25
|
||||||
offset := (page - 1) * limit
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
files, totalCount, err := h.fileService.GetPaginatedFiles(limit, offset)
|
files, totalCount, err := h.fileService.GetPaginatedFiles(limit, offset)
|
||||||
@@ -103,11 +103,6 @@ func (h *Handler) AdminPage(c *gin.Context) {
|
|||||||
return
|
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 {
|
type AdminFileView struct {
|
||||||
file.FileRecord
|
file.FileRecord
|
||||||
ActualStatus string
|
ActualStatus string
|
||||||
@@ -154,3 +149,25 @@ func (h *Handler) Logout(c *gin.Context) {
|
|||||||
func (h *Handler) ChangePasswordPage(c *gin.Context) {
|
func (h *Handler) ChangePasswordPage(c *gin.Context) {
|
||||||
c.HTML(200, "changePassword.html", nil)
|
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.Use(user.ForcePasswordChangeMiddleware(userService))
|
||||||
|
|
||||||
adminRoutes.GET("/admin", h.AdminPage)
|
adminRoutes.GET("/admin", h.AdminPage)
|
||||||
|
adminRoutes.GET("/admin/stats", h.AdminStatsPage)
|
||||||
adminRoutes.GET("/config", h.ConfigPage)
|
adminRoutes.GET("/config", h.ConfigPage)
|
||||||
adminRoutes.POST("/config", h.ConfigSave)
|
adminRoutes.POST("/config", h.ConfigSave)
|
||||||
adminRoutes.GET("/logout", h.Logout)
|
adminRoutes.GET("/logout", h.Logout)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
<div class="flex flex-col items-end gap-2">
|
<div class="flex flex-col items-end gap-2">
|
||||||
<a href="/" class="nav-link">← BACK_TO_UPLOADER</a>
|
<a href="/" class="nav-link">← BACK_TO_UPLOADER</a>
|
||||||
<a href="/config" class="nav-link">CONFIG_MODULE</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>
|
<a href="/logout" class="nav-link text-red-600">LOGOUT_SESSION</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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