feat: maintenance mode #739

This commit is contained in:
Jacky 2025-04-07 18:38:04 +08:00
parent 191ddea309
commit 5d3f478086
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
33 changed files with 3698 additions and 2222 deletions

78
api/pages/maintenance.go Normal file
View file

@ -0,0 +1,78 @@
package pages
import (
"embed"
"html/template"
"net/http"
"strings"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
)
//go:embed *.tmpl
var tmplFS embed.FS
// MaintenancePageData maintenance page data structure
type MaintenancePageData struct {
Title string `json:"title"`
Message string `json:"message"`
Description string `json:"description"`
ICPNumber string `json:"icp_number"`
PublicSecurityNumber string `json:"public_security_number"`
}
const (
Title = "System Maintenance"
Message = "We are currently performing system maintenance to improve your experience."
Description = "Please check back later. Thank you for your understanding and patience."
)
// MaintenancePage returns a maintenance page
func MaintenancePage(c *gin.Context) {
// Prepare template data
data := MaintenancePageData{
Title: Title,
Message: Message,
Description: Description,
ICPNumber: settings.NodeSettings.ICPNumber,
PublicSecurityNumber: settings.NodeSettings.PublicSecurityNumber,
}
// Check User-Agent
userAgent := c.GetHeader("User-Agent")
isBrowser := len(userAgent) > 0 && (contains(userAgent, "Mozilla") ||
contains(userAgent, "Chrome") ||
contains(userAgent, "Safari") ||
contains(userAgent, "Edge") ||
contains(userAgent, "Firefox") ||
contains(userAgent, "Opera"))
if !isBrowser {
c.JSON(http.StatusServiceUnavailable, data)
return
}
// Parse template
tmpl, err := template.ParseFS(tmplFS, "maintenance.tmpl")
if err != nil {
c.String(http.StatusInternalServerError, "503 Service Unavailable")
return
}
// Set content type
c.Header("Content-Type", "text/html; charset=utf-8")
c.Status(http.StatusServiceUnavailable)
// Render template
err = tmpl.Execute(c.Writer, data)
if err != nil {
c.String(http.StatusInternalServerError, "503 Service Unavailable")
return
}
}
// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}

View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<title>{{.Title}} | Nginx UI</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f4f5f7;
color: #333;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
flex-direction: column;
}
.maintenance-container {
max-width: 600px;
padding: 40px;
background: white;
border-radius: 8px;
@media (max-width: 768px) {
border-radius: 0;
}
box-shadow: 0 0 30px #c8c8c840;
margin-bottom: 20px;
}
h1 {
color:rgb(0, 128, 247);
margin-bottom: 20px;
font-size: 28px;
}
a {
color: #1890ff;
text-decoration: none;
}
p {
font-size: 14px;
line-height: 1.6;
margin-bottom: 20px;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
.footer {
margin-top: 30px;
font-size: 12px;
color: #888;
}
.beian-info {
font-size: 12px;
color: #888;
margin-top: 10px;
}
.beian-info p {
margin: 5px 0;
}
.beian-info img {
vertical-align: middle;
margin-right: 5px;
}
</style>
</head>
<body>
<div>
<div class="maintenance-container">
<div class="icon">🛠️</div>
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
<p>{{.Description}}</p>
<div class="footer">
<p>Powered by <a href="https://nginxui.com" target="_blank">Nginx UI</a></p>
</div>
</div>
<div class="beian-info">
{{if .ICPNumber}}
<p><a href="https://beian.miit.gov.cn/" target="_blank">{{.ICPNumber}}</a></p>
{{end}}
{{if .PublicSecurityNumber}}
<p><img src="//www.beian.gov.cn/img/new/gongan.png" alt="公安备案"><a href="http://www.beian.gov.cn/portal/index" target="_blank">{{.PublicSecurityNumber}}</a></p>
{{end}}
</div>
</div>
</body>
</html>

11
api/pages/router.go Normal file
View file

@ -0,0 +1,11 @@
package pages
import (
"github.com/gin-gonic/gin"
)
// InitRouter initializes the pages routes
func InitRouter(r *gin.Engine) {
// Register maintenance page route
r.GET("/pages/maintenance", MaintenancePage)
}

View file

@ -19,7 +19,7 @@ import (
func GetSiteList(c *gin.Context) {
name := c.Query("name")
enabled := c.Query("enabled")
status := c.Query("status")
orderBy := c.Query("sort_by")
sort := c.DefaultQuery("order", "desc")
queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
@ -50,9 +50,23 @@ func GetSiteList(c *gin.Context) {
return filepath.Base(item.Path), item
})
enabledConfigMap := make(map[string]bool)
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
configStatusMap := make(map[string]config.ConfigStatus)
for _, site := range configFiles {
configStatusMap[site.Name()] = config.StatusDisabled
}
// Check for enabled sites and maintenance mode sites
for _, enabledSite := range enabledConfig {
name := enabledSite.Name()
// Check if this is a maintenance mode configuration
if strings.HasSuffix(name, site.MaintenanceSuffix) {
// Extract the original site name by removing maintenance suffix
originalName := strings.TrimSuffix(name, site.MaintenanceSuffix)
configStatusMap[originalName] = config.StatusMaintenance
} else {
configStatusMap[name] = config.StatusEnabled
}
}
var configs []config.Config
@ -68,14 +82,10 @@ func GetSiteList(c *gin.Context) {
continue
}
// status filter
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
if status != "" && configStatusMap[file.Name()] != config.ConfigStatus(status) {
continue
}
var (
envGroupId uint64
envGroup *model.EnvGroup
@ -98,7 +108,7 @@ func GetSiteList(c *gin.Context) {
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
Status: configStatusMap[file.Name()],
EnvGroupID: envGroupId,
EnvGroup: envGroup,
Urls: indexedSite.Urls,

View file

@ -22,4 +22,8 @@ func InitRouter(r *gin.RouterGroup) {
r.DELETE("sites/:name", DeleteSite)
// duplicate site
r.POST("sites/:name/duplicate", DuplicateSite)
// enable maintenance mode for site
r.POST("sites/:name/maintenance/enable", EnableMaintenanceSite)
// disable maintenance mode for site
r.POST("sites/:name/maintenance/disable", DisableMaintenanceSite)
}

View file

@ -215,3 +215,27 @@ func BatchUpdateSites(c *gin.Context) {
ctx.BatchEffectedIDs = effectedPath
}).BatchModify()
}
func EnableMaintenanceSite(c *gin.Context) {
err := site.EnableMaintenance(c.Param("name"))
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
func DisableMaintenanceSite(c *gin.Context) {
err := site.DisableMaintenance(c.Param("name"))
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}

View file

@ -36,7 +36,7 @@ type Stream struct {
func GetStreams(c *gin.Context) {
name := c.Query("name")
enabled := c.Query("enabled")
status := c.Query("status")
orderBy := c.Query("order_by")
sort := c.DefaultQuery("sort", "desc")
queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
@ -53,9 +53,12 @@ func GetStreams(c *gin.Context) {
return
}
enabledConfigMap := make(map[string]bool)
enabledConfigMap := make(map[string]config.ConfigStatus)
for _, file := range configFiles {
enabledConfigMap[file.Name()] = config.StatusDisabled
}
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
enabledConfigMap[enabledConfig[i].Name()] = config.StatusEnabled
}
var configs []config.Config
@ -107,13 +110,8 @@ func GetStreams(c *gin.Context) {
}
// Apply enabled status filter if specified
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
if status != "" && enabledConfigMap[file.Name()] != config.ConfigStatus(status) {
continue
}
var (
@ -138,7 +136,7 @@ func GetStreams(c *gin.Context) {
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
Status: enabledConfigMap[file.Name()],
EnvGroupID: envGroupId,
EnvGroup: envGroup,
})