- 新增 server.request_log 配置:记录业务请求元数据(IP/方法/路径/状态/耗时, key 自动脱敏)与 4xx/5xx 错误摘要(前 4KB),按保留天数自动清理 - 移除 /self-info 与 /vacuum 接口及相关文档 - 状态页重设计:毛玻璃卡片+吸顶玻璃栏、服务状态条、KPI 卡与请求分类 条形图、可折叠 API 文档分组、行内测试面板;深浅色分别调校, 含 reduced-motion / reduced-transparency / 无 backdrop-filter 兜底
266 lines
6.7 KiB
Go
266 lines
6.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
_ "embed"
|
|
"fanqie/internal/algos"
|
|
"fanqie/internal/config"
|
|
"fanqie/internal/fqapi"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// 状态页模板(embed 内嵌,动态数据由 html/template 自动转义)
|
|
//
|
|
//go:embed templates/status.html
|
|
var statusHTML string
|
|
|
|
var statusTmpl = template.Must(template.New("status").Parse(statusHTML))
|
|
|
|
// HandleStatus 状态页面处理
|
|
func HandleStatus(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
uptime := time.Since(startTime)
|
|
|
|
// 格式化运行时间
|
|
days := int(uptime.Hours() / 24)
|
|
hours := int(uptime.Hours()) % 24
|
|
minutes := int(uptime.Minutes()) % 60
|
|
seconds := int(uptime.Seconds()) % 60
|
|
var uptimeStr string
|
|
if days > 0 {
|
|
uptimeStr = fmt.Sprintf("%d天 %d小时 %d分钟 %d秒", days, hours, minutes, seconds)
|
|
} else if hours > 0 {
|
|
uptimeStr = fmt.Sprintf("%d小时 %d分钟 %d秒", hours, minutes, seconds)
|
|
} else if minutes > 0 {
|
|
uptimeStr = fmt.Sprintf("%d分钟 %d秒", minutes, seconds)
|
|
} else {
|
|
uptimeStr = fmt.Sprintf("%d秒", seconds)
|
|
}
|
|
|
|
// 获取签名信息
|
|
currentSign := algos.GlobalSignatureSwitcher.GetCurrentMethod()
|
|
|
|
// 生成签名详情显示
|
|
var signatureDetails string
|
|
if currentSign == "sign2" {
|
|
signatureDetails = "X-Gorgon + X-Khronos"
|
|
} else {
|
|
signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos"
|
|
}
|
|
|
|
// 获取统计数据
|
|
stats := getStatsSnapshot()
|
|
|
|
// 获取设备密钥池状态
|
|
poolStatus := getDeviceKeyPoolStatus()
|
|
|
|
// 获取最近请求(日志未启用时为空)
|
|
var recent []map[string]interface{}
|
|
for _, e := range fqapi.GetRecentRequests(20) {
|
|
recent = append(recent, map[string]interface{}{
|
|
"Time": time.Unix(e.Ts, 0).Format("15:04:05"),
|
|
"IP": e.IP,
|
|
"Method": e.Method,
|
|
"Path": e.Path,
|
|
"Query": truncateStr(e.Query, 80),
|
|
"Status": e.Status,
|
|
"IsErr": e.Status >= 400,
|
|
"Duration": fmt.Sprintf("%dms", e.DurationMs),
|
|
})
|
|
}
|
|
|
|
statusTmpl.Execute(w, map[string]interface{}{
|
|
"NowTime": time.Now().Format("2006-01-02 15:04:05"),
|
|
"Uptime": uptimeStr,
|
|
"StartTime": startTime.Format("2006-01-02 15:04:05"),
|
|
"Signature": signatureDetails,
|
|
"DBTotal": getDBTotalSize(),
|
|
"DBUsedPct": getDBUsagePercentage(),
|
|
"DBUsed": getDBUsedSize(),
|
|
"DBFree": getDBFreeSize(),
|
|
"PoolStatus": poolStatus["status"],
|
|
"PoolTotal": poolStatus["total"],
|
|
"PoolValid": poolStatus["valid"],
|
|
"PoolInvalid": poolStatus["invalid"],
|
|
"PoolIndex": poolStatus["current_index"],
|
|
"StatTotal": stats["total"],
|
|
"StatSearch": stats["search"],
|
|
"StatDetail": stats["detail"],
|
|
"StatCatalog": stats["catalog"],
|
|
"StatContent": stats["content"],
|
|
"StatManga": stats["manga"],
|
|
"StatAudio": stats["audio"],
|
|
"StatVideo": stats["video"],
|
|
"StatCategory": stats["category"],
|
|
"CacheCount": getDBCacheCount(),
|
|
"CacheStatus": getCacheStatus(),
|
|
"CleanupStatus": getCleanupStatus(),
|
|
"CleanupInterval": getCleanupInterval(),
|
|
"RecentRequests": recent,
|
|
})
|
|
}
|
|
|
|
// truncateStr 截断字符串,超长加省略号
|
|
func truncateStr(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n] + "…"
|
|
}
|
|
|
|
// getCacheStatus 获取缓存状态
|
|
func getCacheStatus() string {
|
|
cfg := config.GetConfig()
|
|
if cfg != nil && cfg.Cache.Enabled {
|
|
return "已启用"
|
|
}
|
|
return "已禁用"
|
|
}
|
|
|
|
// getCleanupStatus 获取清理状态
|
|
func getCleanupStatus() string {
|
|
cfg := config.GetConfig()
|
|
if cfg != nil && cfg.Cache.CleanupEnabled {
|
|
return "已启用"
|
|
}
|
|
return "已禁用"
|
|
}
|
|
|
|
// getCleanupInterval 获取清理间隔
|
|
func getCleanupInterval() string {
|
|
cfg := config.GetConfig()
|
|
if cfg != nil {
|
|
seconds := cfg.Cache.CleanupSeconds
|
|
if seconds == 0 {
|
|
return "不清理"
|
|
} else if seconds < 60 {
|
|
return fmt.Sprintf("%d秒", seconds)
|
|
} else if seconds < 3600 {
|
|
return fmt.Sprintf("%d分钟", seconds/60)
|
|
} else {
|
|
return fmt.Sprintf("%d小时", seconds/3600)
|
|
}
|
|
}
|
|
return "未知"
|
|
}
|
|
|
|
// formatBytes 格式化字节大小
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// getDBCacheCount 获取数据库缓存记录数
|
|
func getDBCacheCount() int {
|
|
stats, err := fqapi.GetDatabaseStats()
|
|
if err != nil || stats == nil {
|
|
return 0
|
|
}
|
|
if count, ok := stats["cache_count"].(int); ok {
|
|
return count
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getDBTotalSize 获取数据库总大小
|
|
func getDBTotalSize() string {
|
|
stats, err := fqapi.GetDatabaseStats()
|
|
if err != nil || stats == nil {
|
|
return "未知"
|
|
}
|
|
if size, ok := stats["total_size_bytes"].(int64); ok {
|
|
return formatBytes(size)
|
|
}
|
|
return "未知"
|
|
}
|
|
|
|
// getDBUsedSize 获取数据库已用空间
|
|
func getDBUsedSize() string {
|
|
stats, err := fqapi.GetDatabaseStats()
|
|
if err != nil || stats == nil {
|
|
return "未知"
|
|
}
|
|
if size, ok := stats["used_size_bytes"].(int64); ok {
|
|
return formatBytes(size)
|
|
}
|
|
return "未知"
|
|
}
|
|
|
|
// getDBFreeSize 获取数据库空闲空间(带颜色提示)
|
|
func getDBFreeSize() string {
|
|
stats, err := fqapi.GetDatabaseStats()
|
|
if err != nil || stats == nil {
|
|
return "未知"
|
|
}
|
|
freeSize, okFree := stats["free_size_bytes"].(int64)
|
|
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
|
|
|
if !okFree {
|
|
return "未知"
|
|
}
|
|
|
|
if freeSize == 0 {
|
|
return "0 B"
|
|
}
|
|
|
|
freeSizeStr := formatBytes(freeSize)
|
|
|
|
// 如果有总大小,计算并显示百分比
|
|
if okTotal && totalSize > 0 {
|
|
percentage := float64(freeSize) * 100 / float64(totalSize)
|
|
return fmt.Sprintf("%s (%.1f%%)", freeSizeStr, percentage)
|
|
}
|
|
|
|
return freeSizeStr
|
|
}
|
|
|
|
// getDBUsagePercentage 获取数据库使用率
|
|
func getDBUsagePercentage() float64 {
|
|
stats, err := fqapi.GetDatabaseStats()
|
|
if err != nil || stats == nil {
|
|
return 0
|
|
}
|
|
usedSize, okUsed := stats["used_size_bytes"].(int64)
|
|
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
|
|
|
if !okUsed || !okTotal || totalSize == 0 {
|
|
return 0
|
|
}
|
|
|
|
return float64(usedSize) * 100 / float64(totalSize)
|
|
}
|
|
|
|
// getDeviceKeyPoolStatus 获取设备密钥池状态
|
|
func getDeviceKeyPoolStatus() map[string]interface{} {
|
|
pool := fqapi.GetDeviceKeyPool()
|
|
|
|
if !pool.IsEnabled() {
|
|
return map[string]interface{}{
|
|
"status": "未启用",
|
|
"total": 0,
|
|
"valid": 0,
|
|
"invalid": 0,
|
|
"current_index": 0,
|
|
}
|
|
}
|
|
|
|
stats := pool.GetStats()
|
|
return map[string]interface{}{
|
|
"status": "已启用",
|
|
"total": stats["total"],
|
|
"valid": stats["valid"],
|
|
"invalid": stats["invalid"],
|
|
"current_index": stats["current_index"],
|
|
}
|
|
}
|