mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat: maintenance mode #739
This commit is contained in:
parent
191ddea309
commit
5d3f478086
33 changed files with 3698 additions and 2222 deletions
78
api/pages/maintenance.go
Normal file
78
api/pages/maintenance.go
Normal 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)
|
||||
}
|
89
api/pages/maintenance.tmpl
Normal file
89
api/pages/maintenance.tmpl
Normal 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
11
api/pages/router.go
Normal 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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue