Selaa lähdekoodia

feat: introducing streams management pages #166

0xJacky 1 vuosi sitten
vanhempi
commit
2649b710bb

+ 2 - 1
api/sites/domain.go

@@ -147,7 +147,8 @@ func GetDomain(c *gin.Context) {
 	c.Set("maybe_error", "")
 
 	certInfoMap := make(map[int]*cert.Info)
-	for serverIdx, server := range nginxConfig.Servers {
+
+    for serverIdx, server := range nginxConfig.Servers {
 		for _, directive := range server.Directives {
 			if directive.Directive == "ssl_certificate" {
 

+ 42 - 0
api/streams/advance.go

@@ -0,0 +1,42 @@
+package streams
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func AdvancedEdit(c *gin.Context) {
+	var json struct {
+		Advanced bool `json:"advanced"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	name := c.Param("name")
+	path := nginx.GetConfPath("streams-available", name)
+
+	s := query.Site
+
+	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+
+}

+ 44 - 0
api/streams/duplicate.go

@@ -0,0 +1,44 @@
+package streams
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func Duplicate(c *gin.Context) {
+	// Source name
+	name := c.Param("name")
+
+	// Destination name
+	var json struct {
+		Name string `json:"name" binding:"required"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	src := nginx.GetConfPath("streams-available", name)
+	dst := nginx.GetConfPath("streams-available", json.Name)
+
+	if helper.FileExists(dst) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "File exists",
+		})
+		return
+	}
+
+	_, err := helper.CopyFile(src, dst)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"dst": dst,
+	})
+}

+ 14 - 0
api/streams/router.go

@@ -0,0 +1,14 @@
+package streams
+
+import "github.com/gin-gonic/gin"
+
+func InitRouter(r *gin.RouterGroup) {
+	r.GET("streams", GetStreams)
+	r.GET("stream/:name", GetStream)
+	r.POST("stream/:name", SaveStream)
+	r.POST("stream/:name/enable", EnableStream)
+	r.POST("stream/:name/disable", DisableStream)
+	r.POST("stream/:name/advance", AdvancedEdit)
+	r.DELETE("stream/:name", DeleteStream)
+	r.POST("stream/:name/duplicate", Duplicate)
+}

+ 343 - 0
api/streams/streams.go

@@ -0,0 +1,343 @@
+package streams
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/sashabaranov/go-openai"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+)
+
+type Stream struct {
+	ModifiedAt      time.Time                      `json:"modified_at"`
+	Advanced        bool                           `json:"advanced"`
+	Enabled         bool                           `json:"enabled"`
+	Name            string                         `json:"name"`
+	Config          string                         `json:"config"`
+	ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
+	Tokenized       *nginx.NgxConfig               `json:"tokenized,omitempty"`
+}
+
+func GetStreams(c *gin.Context) {
+	name := c.Query("name")
+	orderBy := c.Query("order_by")
+	sort := c.DefaultQuery("sort", "desc")
+
+	configFiles, err := os.ReadDir(nginx.GetConfPath("streams-available"))
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	enabledConfig, err := os.ReadDir(nginx.GetConfPath("streams-enabled"))
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	enabledConfigMap := make(map[string]bool)
+	for i := range enabledConfig {
+		enabledConfigMap[enabledConfig[i].Name()] = true
+	}
+
+	var configs []config.Config
+
+	for i := range configFiles {
+		file := configFiles[i]
+		fileInfo, _ := file.Info()
+		if !file.IsDir() {
+			if name != "" && !strings.Contains(file.Name(), name) {
+				continue
+			}
+			configs = append(configs, config.Config{
+				Name:       file.Name(),
+				ModifiedAt: fileInfo.ModTime(),
+				Size:       fileInfo.Size(),
+				IsDir:      fileInfo.IsDir(),
+				Enabled:    enabledConfigMap[file.Name()],
+			})
+		}
+	}
+
+	configs = config.Sort(orderBy, sort, configs)
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": configs,
+	})
+}
+
+func GetStream(c *gin.Context) {
+	rewriteName, ok := c.Get("rewriteConfigFileName")
+
+	name := c.Param("name")
+
+	// for modify filename
+	if ok {
+		name = rewriteName.(string)
+	}
+
+	path := nginx.GetConfPath("streams-available", name)
+	file, err := os.Stat(path)
+	if os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "file not found",
+		})
+		return
+	}
+
+	enabled := true
+
+	if _, err := os.Stat(nginx.GetConfPath("streams-enabled", name)); os.IsNotExist(err) {
+		enabled = false
+	}
+
+	s := query.Stream
+	stream, err := s.Where(s.Path.Eq(path)).FirstOrInit()
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if stream.Advanced {
+		origContent, err := os.ReadFile(path)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+
+		c.JSON(http.StatusOK, Stream{
+			ModifiedAt:      file.ModTime(),
+			Advanced:        stream.Advanced,
+			Enabled:         enabled,
+			Name:            name,
+			Config:          string(origContent),
+			ChatGPTMessages: stream.ChatGPTMessages,
+		})
+		return
+	}
+
+	c.Set("maybe_error", "nginx_config_syntax_error")
+	nginxConfig, err := nginx.ParseNgxConfig(path)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.Set("maybe_error", "nginx_config_syntax_error")
+
+	c.JSON(http.StatusOK, Stream{
+		ModifiedAt:      file.ModTime(),
+		Advanced:        stream.Advanced,
+		Enabled:         enabled,
+		Name:            name,
+		Config:          nginxConfig.FmtCode(),
+		Tokenized:       nginxConfig,
+		ChatGPTMessages: stream.ChatGPTMessages,
+	})
+}
+
+func SaveStream(c *gin.Context) {
+	name := c.Param("name")
+
+	if name == "" {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "param name is empty",
+		})
+		return
+	}
+
+	var json struct {
+		Name      string `json:"name" binding:"required"`
+		Content   string `json:"content" binding:"required"`
+		Overwrite bool   `json:"overwrite"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	path := nginx.GetConfPath("streams-available", name)
+
+	if !json.Overwrite && helper.FileExists(path) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "File exists",
+		})
+		return
+	}
+
+	err := os.WriteFile(path, []byte(json.Content), 0644)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
+	// rename the config file if needed
+	if name != json.Name {
+		newPath := nginx.GetConfPath("streams-available", json.Name)
+		s := query.Stream
+		_, err = s.Where(s.Path.Eq(path)).Update(s.Path, newPath)
+
+		// check if dst file exists, do not rename
+		if helper.FileExists(newPath) {
+			c.JSON(http.StatusNotAcceptable, gin.H{
+				"message": "File exists",
+			})
+			return
+		}
+		// recreate a soft link
+		if helper.FileExists(enabledConfigFilePath) {
+			_ = os.Remove(enabledConfigFilePath)
+			enabledConfigFilePath = nginx.GetConfPath("streams-enabled", json.Name)
+			err = os.Symlink(newPath, enabledConfigFilePath)
+
+			if err != nil {
+				api.ErrHandler(c, err)
+				return
+			}
+		}
+
+		err = os.Rename(path, newPath)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+
+		name = json.Name
+		c.Set("rewriteConfigFileName", name)
+	}
+
+	enabledConfigFilePath = nginx.GetConfPath("streams-enabled", name)
+	if helper.FileExists(enabledConfigFilePath) {
+		// Test nginx configuration
+		output := nginx.TestConf()
+
+		if nginx.GetLogLevel(output) > nginx.Warn {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"message": output,
+				"error":   "nginx_config_syntax_error",
+			})
+			return
+		}
+
+		output = nginx.Reload()
+
+		if nginx.GetLogLevel(output) > nginx.Warn {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"message": output,
+			})
+			return
+		}
+	}
+
+	GetStream(c)
+}
+
+func EnableStream(c *gin.Context) {
+	configFilePath := nginx.GetConfPath("streams-available", c.Param("name"))
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name"))
+
+	_, err := os.Stat(configFilePath)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+		err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+	}
+
+	// Test nginx config, if not pass, then disable the stream.
+	output := nginx.TestConf()
+
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		_ = os.Remove(enabledConfigFilePath)
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	output = nginx.Reload()
+
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}
+
+func DisableStream(c *gin.Context) {
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name"))
+
+	_, err := os.Stat(enabledConfigFilePath)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	err = os.Remove(enabledConfigFilePath)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	output := nginx.Reload()
+
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}
+
+func DeleteStream(c *gin.Context) {
+	var err error
+	name := c.Param("name")
+	availablePath := nginx.GetConfPath("streams-available", name)
+	enabledPath := nginx.GetConfPath("streams-enabled", name)
+
+	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "stream not found",
+		})
+		return
+	}
+
+	if _, err = os.Stat(enabledPath); err == nil {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "stream is enabled",
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 37 - 0
app/src/api/stream.ts

@@ -0,0 +1,37 @@
+import Curd from '@/api/curd'
+import http from '@/lib/http'
+import type { ChatComplicationMessage } from '@/api/openai'
+import type { NgxConfig } from '@/api/ngx'
+
+export interface Stream {
+  modified_at: string
+  advanced: boolean
+  enabled: boolean
+  name: string
+  config: string
+  chatgpt_messages: ChatComplicationMessage[]
+  tokenized?: NgxConfig
+}
+
+class StreamCurd extends Curd<Stream> {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  enable(name: string, config?: any) {
+    return http.post(`${this.baseUrl}/${name}/enable`, undefined, config)
+  }
+
+  disable(name: string) {
+    return http.post(`${this.baseUrl}/${name}/disable`)
+  }
+
+  duplicate(name: string, data: { name: string }): Promise<{ dst: string }> {
+    return http.post(`${this.baseUrl}/${name}/duplicate`, data)
+  }
+
+  advance_mode(name: string, data: { advanced: boolean }) {
+    return http.post(`${this.baseUrl}/${name}/advance`, data)
+  }
+}
+
+const stream = new StreamCurd('/stream')
+
+export default stream

+ 162 - 72
app/src/language/en/app.po

@@ -9,26 +9,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "About"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr ""
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "Action"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr ""
 
@@ -41,16 +43,26 @@ msgstr "Add Directive Below"
 msgid "Add Location"
 msgstr "Add Location"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "Add Site"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "Add Site"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "Saved successfully"
+
 #: src/views/certificate/DNSChallenge.vue:94
 #, fuzzy
 msgid "Additional"
 msgstr "Add Location"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "Advance Mode"
 
@@ -82,12 +94,12 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "Are you sure you want to remove this directive?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 #, fuzzy
 msgid "Are you sure you want to delete?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "Are you sure you want to remove this directive?"
 
@@ -128,6 +140,7 @@ msgstr "Auto-renewal enabled for %{name}"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "Back"
 
@@ -143,11 +156,12 @@ msgstr "Base information"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 #, fuzzy
 msgid "Basic"
 msgstr "Basic Mode"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "Basic Mode"
 
@@ -172,9 +186,11 @@ msgstr ""
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "Cancel"
 
@@ -191,12 +207,12 @@ msgstr "Certificate is valid"
 msgid "Certificate Status"
 msgstr "Certificate Status"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "Certificate Status"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "Certificate is valid"
@@ -243,10 +259,10 @@ msgstr ""
 msgid "Cleared successfully"
 msgstr "Disabled successfully"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "Comments"
 
@@ -275,7 +291,7 @@ msgstr "Configure SSL"
 msgid "Connected"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -293,6 +309,11 @@ msgstr "CPU Status"
 msgid "CPU:"
 msgstr "CPU:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "Created at"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "Create Another"
@@ -318,11 +339,11 @@ msgid "Current Version"
 msgstr ""
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr ""
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "Dashboard"
 
@@ -332,8 +353,9 @@ msgstr "Database (Optional, default: database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr ""
 
@@ -341,6 +363,10 @@ msgstr ""
 msgid "Delete site: %{site_name}"
 msgstr ""
 
+#: src/views/stream/StreamList.vue:81
+msgid "Delete stream: %{stream_name}"
+msgstr ""
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -348,18 +374,23 @@ msgstr "Disabled successfully"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 #, fuzzy
 msgid "Deploy successfully"
 msgstr "Saved successfully"
@@ -381,7 +412,7 @@ msgstr "Development Mode"
 msgid "Directive"
 msgstr "Directive"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "Directives"
 
@@ -390,7 +421,7 @@ msgstr "Directives"
 msgid "Directory"
 msgstr "Directive"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 #, fuzzy
 msgid "Disable"
 msgstr "Disabled"
@@ -400,12 +431,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "Disable auto-renewal failed for %{name}"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "Disabled"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "Disabled successfully"
 
@@ -413,7 +447,7 @@ msgstr "Disabled successfully"
 msgid "Disk IO"
 msgstr "Disk IO"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr ""
 
@@ -427,6 +461,7 @@ msgid "DNS01"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 #, fuzzy
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
@@ -442,22 +477,32 @@ msgstr ""
 msgid "Do you want to disable this site?"
 msgstr "Are you sure you want to remove this directive?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "Are you sure you want to remove this directive?"
+
 #: src/views/domain/components/RightSettings.vue:48
 #, fuzzy
 msgid "Do you want to enable this site?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "Are you sure you want to remove this directive?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 #, fuzzy
 msgid "Do you want to enable TLS?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 #, fuzzy
 msgid "Do you want to remove this server?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "Are you sure you want to remove this directive?"
@@ -488,56 +533,71 @@ msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 #, fuzzy
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "Saved successfully"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 #, fuzzy
 msgid "Duplicate failed"
 msgstr "Enable failed"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 #, fuzzy
 msgid "Duplicate successfully"
 msgstr "Saved successfully"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 #, fuzzy
 msgid "Duplicate to local successfully"
 msgstr "Saved successfully"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "Edit %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "Edit Configuration"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "Edit Site"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "Edit Site"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "Email (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 #, fuzzy
 msgid "Enable"
 msgstr "Enabled"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
@@ -550,23 +610,29 @@ msgid "Enable failed"
 msgstr "Enable failed"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 #, fuzzy
 msgid "Enable successfully"
 msgstr "Enabled successfully"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "Enable TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "Enabled"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "Enabled successfully"
 
@@ -574,7 +640,7 @@ msgstr "Enabled successfully"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Encrypt website with Let's Encrypt"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr ""
 
@@ -587,7 +653,7 @@ msgstr "Comments"
 msgid "Error"
 msgstr ""
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr ""
 
@@ -611,11 +677,15 @@ msgstr ""
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "Failed to disable %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "Failed to enable %{msg}"
 
@@ -623,7 +693,7 @@ msgstr "Failed to enable %{msg}"
 msgid "Failed to get certificate information"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 
@@ -703,7 +773,7 @@ msgstr ""
 msgid "GPT-4-Turbo"
 msgstr ""
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "Home"
 
@@ -727,7 +797,7 @@ msgstr ""
 msgid "Import"
 msgstr ""
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Certificate Status"
@@ -744,7 +814,7 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "Install"
 
@@ -827,7 +897,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Login"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "Login"
 
@@ -848,15 +918,20 @@ msgstr ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "Manage Configs"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "Manage Sites"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "Manage Sites"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "Manage Users"
 
@@ -880,7 +955,7 @@ msgstr "Memory and Storage"
 msgid "Modify"
 msgstr "Modify Config"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Certificate Status"
@@ -901,8 +976,11 @@ msgstr "Single Directive"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "Name"
 
@@ -940,7 +1018,7 @@ msgstr ""
 msgid "Nginx Access Log Path"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 #, fuzzy
 msgid "Nginx Configuration Parse Error"
 msgstr "Configuration Name"
@@ -953,7 +1031,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr ""
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr ""
 
@@ -971,9 +1049,10 @@ msgstr "Saved successfully"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "No"
 
@@ -985,7 +1064,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "Not Found"
 
@@ -1003,7 +1082,7 @@ msgstr ""
 msgid "Notification"
 msgstr "Certificate is valid"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "Certificate is valid"
@@ -1033,10 +1112,13 @@ msgstr ""
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr ""
 
@@ -1065,10 +1147,12 @@ msgid "OS:"
 msgstr "OS:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr ""
 
@@ -1111,6 +1195,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1134,6 +1219,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr ""
 
@@ -1141,7 +1227,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr ""
 
@@ -1211,15 +1297,11 @@ msgstr ""
 msgid "Removed successfully"
 msgstr "Saved successfully"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "Username"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1270,8 +1352,8 @@ msgstr ""
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "Save"
 
@@ -1280,7 +1362,7 @@ msgid "Save Directive"
 msgstr "Save Directive"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "Save error %{msg}"
 
@@ -1298,7 +1380,8 @@ msgstr "Saved successfully"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "Saved successfully"
 
@@ -1319,6 +1402,7 @@ msgstr "Send"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "Server error"
@@ -1352,12 +1436,12 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Single Directive"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 #, fuzzy
 msgid "Site Logs"
 msgstr "Sites List"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "Sites List"
 
@@ -1394,7 +1478,7 @@ msgid "Stable"
 msgstr "Enabled"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "Status"
 
@@ -1427,7 +1511,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr ""
 
@@ -1437,10 +1521,11 @@ msgid "Table"
 msgstr "Enabled"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -1490,7 +1575,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1507,7 +1592,8 @@ msgstr ""
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "Updated at"
 
@@ -1516,7 +1602,7 @@ msgstr "Updated at"
 msgid "Updated successfully"
 msgstr "Saved successfully"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr ""
@@ -1530,6 +1616,10 @@ msgstr "Saved successfully"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr ""
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "Uptime:"
@@ -1595,7 +1685,7 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "Yes"

+ 163 - 72
app/src/language/es/app.po

@@ -14,26 +14,28 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 5.0\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "Acerca de"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "Registros de acceso"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "Acción"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "Agregar"
 
@@ -46,15 +48,25 @@ msgstr "Añadir directiva a continuación"
 msgid "Add Location"
 msgstr "Agregar Ubicación"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "Agregar Sitio"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "Agregar Sitio"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "Actualización exitosa"
+
 #: src/views/certificate/DNSChallenge.vue:94
 msgid "Additional"
 msgstr "Adicional"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "Modo avanzado"
 
@@ -85,11 +97,11 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "¿Está seguro de que desea borrar el registro del chat?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 msgid "Are you sure you want to delete?"
 msgstr "¿Está seguro de que quiere borrar?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "¿Está seguro de que quiere borrar esta directiva?"
 
@@ -129,6 +141,7 @@ msgstr "Renovación automática habilitada por %{name}"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "Volver"
 
@@ -143,10 +156,11 @@ msgstr "Información general"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 msgid "Basic"
 msgstr "Básico"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "Modo Básico"
 
@@ -170,9 +184,11 @@ msgstr "Directorio CA"
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "Cancelar"
 
@@ -189,12 +205,12 @@ msgstr "El certificado es válido"
 msgid "Certificate Status"
 msgstr "Estado del Certificado"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "Estado del Certificado"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "Lista de Certificados"
@@ -240,10 +256,10 @@ msgstr "Limpiar"
 msgid "Cleared successfully"
 msgstr "Desactivado con éxito"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "Comentarios"
 
@@ -271,7 +287,7 @@ msgstr "Configurar SSL"
 msgid "Connected"
 msgstr "Conectado"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -289,6 +305,11 @@ msgstr "Estado del CPU"
 msgid "CPU:"
 msgstr "CPU:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "Creado el"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "Crear otro"
@@ -314,11 +335,11 @@ msgid "Current Version"
 msgstr "Versión actual"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "Personalizado"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "Panel"
 
@@ -328,8 +349,9 @@ msgstr "Base de datos (Opcional, default: database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "Eliminar"
 
@@ -337,6 +359,11 @@ msgstr "Eliminar"
 msgid "Delete site: %{site_name}"
 msgstr "Eliminar sitio: %{site_name}"
 
+#: src/views/stream/StreamList.vue:81
+#, fuzzy
+msgid "Delete stream: %{stream_name}"
+msgstr "Eliminar sitio: %{site_name}"
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -344,18 +371,23 @@ msgstr "Desactivado con éxito"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr "Desplegar"
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr "Falló el desplegado de %{conf_name} a %{node_name}"
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr "Desplegado de %{conf_name} a %{node_name} exitoso"
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 msgid "Deploy successfully"
 msgstr "Desplegado con éxito"
 
@@ -376,7 +408,7 @@ msgstr "Modo de desarrollo"
 msgid "Directive"
 msgstr "Directiva"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "Directivas"
 
@@ -385,7 +417,7 @@ msgstr "Directivas"
 msgid "Directory"
 msgstr "Directiva"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 #, fuzzy
 msgid "Disable"
 msgstr "Desactivado"
@@ -395,12 +427,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "No se pudo desactivar la renovación automática por %{name}"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "Desactivado"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "Desactivado con éxito"
 
@@ -408,7 +443,7 @@ msgstr "Desactivado con éxito"
 msgid "Disk IO"
 msgstr "I/O del disco"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr "Credenciales de DNS"
 
@@ -422,6 +457,7 @@ msgid "DNS01"
 msgstr "DNS01"
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
 msgstr[0] "¿Desea desplegar este archivo en un servidor remoto?"
@@ -435,19 +471,29 @@ msgstr "¿Desea deshabilitar la renovación automática de certificado?"
 msgid "Do you want to disable this site?"
 msgstr "¿Quieres deshabilitar este sitio?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "¿Quieres deshabilitar este sitio?"
+
 #: src/views/domain/components/RightSettings.vue:48
 msgid "Do you want to enable this site?"
 msgstr "¿Quieres habilitar este sitio?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "¿Quieres habilitar este sitio?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 msgid "Do you want to enable TLS?"
 msgstr "¿Quieres habilitar TLS?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 msgid "Do you want to remove this server?"
 msgstr "¿Quieres eliminar este servidor?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "¿Quieres eliminar este servidor?"
@@ -481,52 +527,67 @@ msgstr "Modo de ejecución de prueba habilitado"
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "Duplicar"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "Duplicado con éxito de %{conf_name} a %{node_name}"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 msgid "Duplicate failed"
 msgstr "Duplicado fallido"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 msgid "Duplicate successfully"
 msgstr "Duplicado con éxito"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 msgid "Duplicate to local successfully"
 msgstr "Duplicado con éxito a local"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "Editar %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "Editar Configuración"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "Editar Sitio"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "Editar Sitio"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "Correo (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 #, fuzzy
 msgid "Enable"
 msgstr "Habilitado"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr "Falló el habilitado de %{conf_name} en %{node_name}"
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Habilitado exitoso de %{conf_name} en %{node_name}"
 
@@ -539,22 +600,28 @@ msgid "Enable failed"
 msgstr "Falló la habilitación"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 msgid "Enable successfully"
 msgstr "Habilitado con éxito"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "Habilitar TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "Habilitado"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "Habilitado con éxito"
 
@@ -562,7 +629,7 @@ msgstr "Habilitado con éxito"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Encriptar sitio web con Let's Encrypt"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr "Entorno"
 
@@ -574,7 +641,7 @@ msgstr "Entornos"
 msgid "Error"
 msgstr "Error"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "Registros de acceso"
 
@@ -598,11 +665,15 @@ msgstr "Exportar"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "Error al deshabilitar %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "Error al habilitar %{msg}"
 
@@ -610,7 +681,7 @@ msgstr "Error al habilitar %{msg}"
 msgid "Failed to get certificate information"
 msgstr "No se pudo obtener la información del certificado"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 "No se pudo guardar, se detectó un error(es) de sintaxis en la configuración."
@@ -689,7 +760,7 @@ msgstr "GPT-4-32K"
 msgid "GPT-4-Turbo"
 msgstr "GPT-3.5-Turbo"
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "Inicio"
 
@@ -714,7 +785,7 @@ msgstr "HTTP01"
 msgid "Import"
 msgstr "Exportar"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Estado del Certificado"
@@ -731,7 +802,7 @@ msgstr "Error de actualización de kernel inicial"
 msgid "Initialing core upgrader"
 msgstr "Inicializando la actualización del kernel"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "Instalar"
 
@@ -811,7 +882,7 @@ msgstr "Ubicaciones"
 msgid "Log"
 msgstr "Acceso"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "Acceso"
 
@@ -831,15 +902,20 @@ msgstr ""
 "Asegúrese de haber configurado un proxy reverso para el directorio .well-"
 "known en HTTPChallengePort antes de obtener el certificado."
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "Administrar configuraciones"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "Administrar sitios"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "Administrar sitios"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "Administrar usuarios"
 
@@ -862,7 +938,7 @@ msgstr "Memoria y almacenamiento"
 msgid "Modify"
 msgstr "Modificar"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Estado del Certificado"
@@ -882,8 +958,11 @@ msgstr "Directiva multilínea"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "Nombre"
 
@@ -921,7 +1000,7 @@ msgstr "Nginx"
 msgid "Nginx Access Log Path"
 msgstr "Ruta de registro de acceso de Nginx"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 msgid "Nginx Configuration Parse Error"
 msgstr "Error de análisis de configuración de Nginx"
 
@@ -933,7 +1012,7 @@ msgstr "Control de Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Ruta de registro de errores de Nginx"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr "Registro Nginx"
 
@@ -949,9 +1028,10 @@ msgstr "Nginx reiniciado con éxito"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "No"
 
@@ -963,7 +1043,7 @@ msgstr "Secreto del nodo"
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "No encontrado"
 
@@ -981,7 +1061,7 @@ msgstr "Nota"
 msgid "Notification"
 msgstr "Certificación"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "Certificación"
@@ -1010,10 +1090,13 @@ msgstr "Desconectado"
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr "OK"
 
@@ -1041,10 +1124,12 @@ msgid "OS:"
 msgstr "SO:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr "Sobrescribir"
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr "Sobrescribir archivo existente"
 
@@ -1091,6 +1176,7 @@ msgstr ""
 "del proveedor de DNS."
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1116,6 +1202,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr "¡Seleccione al menos un nodo!"
 
@@ -1123,7 +1210,7 @@ msgstr "¡Seleccione al menos un nodo!"
 msgid "Pre-release"
 msgstr "Prelanzamiento"
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "Configuración"
 
@@ -1191,15 +1278,11 @@ msgstr "Recargando Nginx"
 msgid "Removed successfully"
 msgstr "Guardado con éxito"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "Nombre de usuario"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1249,8 +1332,8 @@ msgstr "Corriendo"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "Guardar"
 
@@ -1259,7 +1342,7 @@ msgid "Save Directive"
 msgstr "Guardar Directiva"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "Error al guardar %{msg}"
 
@@ -1275,7 +1358,8 @@ msgstr "Guardado con éxito"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "Guardado con éxito"
 
@@ -1296,6 +1380,7 @@ msgstr "Enviado"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "Error del servidor"
@@ -1331,11 +1416,11 @@ msgstr "Usando el proveedor de desafíos HTTP01"
 msgid "Single Directive"
 msgstr "Directiva de una sola línea"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 msgid "Site Logs"
 msgstr "Registros del sitio"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "Lista de sitios"
 
@@ -1369,7 +1454,7 @@ msgid "Stable"
 msgstr "Estable"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "Estado"
 
@@ -1402,7 +1487,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "Sistema"
 
@@ -1411,10 +1496,11 @@ msgid "Table"
 msgstr "Tabla"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr "Objetivo"
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -1466,7 +1552,7 @@ msgstr "Este campo no debe estar vacío"
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1487,7 +1573,8 @@ msgstr "Tipo"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "Actualizado a"
 
@@ -1495,7 +1582,7 @@ msgstr "Actualizado a"
 msgid "Updated successfully"
 msgstr "Actualización exitosa"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "Actualizar"
@@ -1508,6 +1595,10 @@ msgstr "Actualización exitosa"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "Actualizando Nginx UI, por favor espere..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "Tiempo encendido:"
@@ -1574,7 +1665,7 @@ msgstr "Escribir la clave privada del certificado a disco"
 msgid "Writing certificate to disk"
 msgstr "Escribir certificado a disco"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "Si"

+ 163 - 72
app/src/language/fr_FR/app.po

@@ -11,26 +11,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "X-Generator: Poedit 3.3\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "À propos"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "Journaux d'accès"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "Action"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "Ajouter"
 
@@ -43,16 +45,26 @@ msgstr "Ajouter une directive"
 msgid "Add Location"
 msgstr "Ajouter une localisation"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "Ajouter un site"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "Ajouter un site"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "Mis à jour avec succés"
+
 #: src/views/certificate/DNSChallenge.vue:94
 #, fuzzy
 msgid "Additional"
 msgstr "Supplémentaire"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "Mode avancé"
 
@@ -84,11 +96,11 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "Voulez-vous vraiment effacer l'historique du chat ?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 msgid "Are you sure you want to delete?"
 msgstr "Etes-vous sûr que vous voulez supprimer ?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "Voulez-vous vraiment supprimer cette directive ?"
 
@@ -129,6 +141,7 @@ msgstr "Renouvellement automatique activé pour %{name}"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "Retour"
 
@@ -143,10 +156,11 @@ msgstr "Information générale"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 msgid "Basic"
 msgstr "Basique"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "Mode simple"
 
@@ -171,9 +185,11 @@ msgstr ""
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "Annuler"
 
@@ -190,12 +206,12 @@ msgstr "Le certificat est valide"
 msgid "Certificate Status"
 msgstr "État du certificat"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "État du certificat"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "Liste des certifications"
@@ -241,10 +257,10 @@ msgstr "Effacer"
 msgid "Cleared successfully"
 msgstr "Désactivé avec succès"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "Commentaires"
 
@@ -272,7 +288,7 @@ msgstr "Configurer SSL"
 msgid "Connected"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -290,6 +306,11 @@ msgstr "État du processeur"
 msgid "CPU:"
 msgstr "CPU :"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "Créé le"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "Créer un autre"
@@ -315,11 +336,11 @@ msgid "Current Version"
 msgstr "Version actuelle"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "Custom"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "Dashboard"
 
@@ -329,8 +350,9 @@ msgstr "Base de données (Facultatif, par défaut : database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "Supprimer"
 
@@ -338,6 +360,11 @@ msgstr "Supprimer"
 msgid "Delete site: %{site_name}"
 msgstr "Supprimer le site : %{site_name}"
 
+#: src/views/stream/StreamList.vue:81
+#, fuzzy
+msgid "Delete stream: %{stream_name}"
+msgstr "Supprimer le site : %{site_name}"
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -345,18 +372,23 @@ msgstr "Désactivé avec succès"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 #, fuzzy
 msgid "Deploy successfully"
 msgstr "Sauvegarde réussie"
@@ -378,7 +410,7 @@ msgstr "Mode développement"
 msgid "Directive"
 msgstr "Directive"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "Directives"
 
@@ -387,7 +419,7 @@ msgstr "Directives"
 msgid "Directory"
 msgstr "Directive"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 #, fuzzy
 msgid "Disable"
 msgstr "Désactivé"
@@ -397,12 +429,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "La désactivation du renouvellement automatique a échoué pour %{name}"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "Désactivé"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "Désactivé avec succès"
 
@@ -410,7 +445,7 @@ msgstr "Désactivé avec succès"
 msgid "Disk IO"
 msgstr "E/S disque"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr "Identifiants DNS"
 
@@ -424,6 +459,7 @@ msgid "DNS01"
 msgstr "DNS01"
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 #, fuzzy
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
@@ -438,19 +474,29 @@ msgstr "Voulez-vous désactiver le renouvellement automatique des certificats ?"
 msgid "Do you want to disable this site?"
 msgstr "Voulez-vous désactiver ce site ?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "Voulez-vous désactiver ce site ?"
+
 #: src/views/domain/components/RightSettings.vue:48
 msgid "Do you want to enable this site?"
 msgstr "Voulez-vous activer ce site ?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "Voulez-vous activer ce site ?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 msgid "Do you want to enable TLS?"
 msgstr "Voulez-vous activer TLS ?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 msgid "Do you want to remove this server?"
 msgstr "Voulez-vous supprimer ce serveur ?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "Voulez-vous supprimer ce serveur ?"
@@ -484,56 +530,71 @@ msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "Dupliquer"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 #, fuzzy
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "Dupliqué avec succès"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 #, fuzzy
 msgid "Duplicate failed"
 msgstr "Dupliquer"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 #, fuzzy
 msgid "Duplicate successfully"
 msgstr "Dupliqué avec succès"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 #, fuzzy
 msgid "Duplicate to local successfully"
 msgstr "Dupliqué avec succès"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "Modifier %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "Modifier la configuration"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "Modifier le site"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "Modifier le site"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "Email (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 #, fuzzy
 msgid "Enable"
 msgstr "Activé"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
@@ -546,23 +607,29 @@ msgid "Enable failed"
 msgstr "Échec de l'activation"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 #, fuzzy
 msgid "Enable successfully"
 msgstr "Activé avec succès"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "Activer TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "Activé"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "Activé avec succès"
 
@@ -570,7 +637,7 @@ msgstr "Activé avec succès"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Crypter le site Web avec Let's Encrypt"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr ""
 
@@ -583,7 +650,7 @@ msgstr "Commentaires"
 msgid "Error"
 msgstr "Erreur"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "Journaux d'erreurs"
 
@@ -607,11 +674,15 @@ msgstr "Exporter"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "Impossible de désactiver %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "Impossible d'activer %{msg}"
 
@@ -619,7 +690,7 @@ msgstr "Impossible d'activer %{msg}"
 msgid "Failed to get certificate information"
 msgstr "Échec de l'obtention des informations sur le certificat"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 "Échec de l'enregistrement, une ou plusieurs erreurs de syntaxe ont été "
@@ -699,7 +770,7 @@ msgstr "GPT-4-32K"
 msgid "GPT-4-Turbo"
 msgstr "GPT-3.5-Turbo"
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "Menu principal"
 
@@ -724,7 +795,7 @@ msgstr "HTTP01"
 msgid "Import"
 msgstr "Exporter"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "État du certificat"
@@ -741,7 +812,7 @@ msgstr "Erreur du programme de mise à niveau initial du core"
 msgid "Initialing core upgrader"
 msgstr "Initialisation du programme de mise à niveau du core"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "Installer"
 
@@ -824,7 +895,7 @@ msgstr "Localisations"
 msgid "Log"
 msgstr "Connexion"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "Connexion"
 
@@ -845,15 +916,20 @@ msgstr ""
 "Assurez vous d'avoir configuré un reverse proxy pour le répertoire .well-"
 "known vers HTTPChallengePort avant d'obtenir le certificat."
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "Gérer les configurations"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "Gérer les sites"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "Gérer les sites"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "Gérer les utilisateurs"
 
@@ -876,7 +952,7 @@ msgstr "Mémoire et stockage"
 msgid "Modify"
 msgstr "Modifier"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "État du certificat"
@@ -896,8 +972,11 @@ msgstr "Directive multiligne"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "Nom"
 
@@ -936,7 +1015,7 @@ msgstr "Journal Nginx"
 msgid "Nginx Access Log Path"
 msgstr "Chemin du journal d'accès Nginx"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 msgid "Nginx Configuration Parse Error"
 msgstr "Erreur d'analyse de configuration Nginx"
 
@@ -948,7 +1027,7 @@ msgstr "Contrôle Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Chemin du journal des erreurs Nginx"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr "Journal Nginx"
 
@@ -964,9 +1043,10 @@ msgstr "Nginx a redémarré avec succès"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "Non"
 
@@ -979,7 +1059,7 @@ msgstr "Secret Jwt"
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "Introuvable"
 
@@ -997,7 +1077,7 @@ msgstr "Note"
 msgid "Notification"
 msgstr "Certification"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "Certification"
@@ -1026,10 +1106,13 @@ msgstr ""
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr "OK"
 
@@ -1057,10 +1140,12 @@ msgid "OS:"
 msgstr "OS :"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr ""
 
@@ -1108,6 +1193,7 @@ msgstr ""
 "fournisseur DNS."
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1133,6 +1219,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr ""
 
@@ -1140,7 +1227,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "Préférence"
 
@@ -1211,15 +1298,11 @@ msgstr "Rechargement de nginx"
 msgid "Removed successfully"
 msgstr "Enregistré avec succès"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "Nom d'utilisateur"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1269,8 +1352,8 @@ msgstr "En cours d'éxécution"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "Enregistrer"
 
@@ -1279,7 +1362,7 @@ msgid "Save Directive"
 msgstr "Enregistrer la directive"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "Enregistrer l'erreur %{msg}"
 
@@ -1295,7 +1378,8 @@ msgstr "Sauvegarde Réussie"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "Enregistré avec succès"
 
@@ -1316,6 +1400,7 @@ msgstr "Envoyer"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "Erreur du serveur"
@@ -1351,11 +1436,11 @@ msgstr "Utilisation du fournisseur de challenge HTTP01"
 msgid "Single Directive"
 msgstr "Directive unique"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 msgid "Site Logs"
 msgstr "Journaux du site"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "Liste des sites"
 
@@ -1390,7 +1475,7 @@ msgid "Stable"
 msgstr "Tableau"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "Statut"
 
@@ -1424,7 +1509,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "Système"
 
@@ -1433,10 +1518,11 @@ msgid "Table"
 msgstr "Tableau"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -1490,7 +1576,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1511,7 +1597,8 @@ msgstr "Type"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "Mis à jour le"
 
@@ -1519,7 +1606,7 @@ msgstr "Mis à jour le"
 msgid "Updated successfully"
 msgstr "Mis à jour avec succés"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "Mettre à niveau"
@@ -1532,6 +1619,10 @@ msgstr "Mise à niveau réussie"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "Mise à jour de Nginx UI, veuillez patienter..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "Disponibilité :"
@@ -1599,7 +1690,7 @@ msgstr "Écriture de la clé privée du certificat sur le disque"
 msgid "Writing certificate to disk"
 msgstr "Écriture du certificat sur le disque"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "Oui"

+ 159 - 61
app/src/language/messages.pot

@@ -2,11 +2,11 @@ msgid ""
 msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr ""
 
-#: src/routes/index.ts:152
+#: src/routes/index.ts:169
 #: src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr ""
@@ -17,6 +17,7 @@ msgstr ""
 #: src/views/domain/DomainList.vue:50
 #: src/views/environment/Environment.vue:105
 #: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50
 #: src/views/user/User.vue:46
 msgid "Action"
 msgstr ""
@@ -24,8 +25,9 @@ msgstr ""
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr ""
 
@@ -38,16 +40,25 @@ msgstr ""
 msgid "Add Location"
 msgstr ""
 
-#: src/routes/index.ts:71
+#: src/routes/index.ts:72
 #: src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr ""
 
+#: src/views/stream/StreamList.vue:184
+msgid "Add Stream"
+msgstr ""
+
+#: src/views/stream/StreamList.vue:114
+msgid "Added successfully"
+msgstr ""
+
 #: src/views/certificate/DNSChallenge.vue:94
 msgid "Additional"
 msgstr ""
 
 #: src/views/domain/DomainEdit.vue:204
+#: src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr ""
 
@@ -78,10 +89,11 @@ msgstr ""
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
 #: src/views/domain/DomainList.vue:147
+#: src/views/stream/StreamList.vue:168
 msgid "Are you sure you want to delete?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr ""
 
@@ -123,6 +135,7 @@ msgstr ""
 #: src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261
 #: src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr ""
 
@@ -137,10 +150,12 @@ msgstr ""
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 msgid "Basic"
 msgstr ""
 
 #: src/views/domain/DomainEdit.vue:207
+#: src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr ""
 
@@ -164,9 +179,11 @@ msgstr ""
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr ""
 
@@ -183,12 +200,12 @@ msgstr ""
 msgid "Certificate Status"
 msgstr ""
 
-#: src/routes/index.ts:101
+#: src/routes/index.ts:118
 #: src/views/certificate/Certificate.vue:122
 msgid "Certificates"
 msgstr ""
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 msgid "Certificates List"
 msgstr ""
 
@@ -232,10 +249,10 @@ msgstr ""
 msgid "Cleared successfully"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr ""
 
@@ -263,7 +280,7 @@ msgstr ""
 msgid "Connected"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -281,6 +298,10 @@ msgstr ""
 msgid "CPU:"
 msgstr ""
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+msgid "Create"
+msgstr ""
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr ""
@@ -307,11 +328,11 @@ msgid "Current Version"
 msgstr ""
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr ""
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr ""
 
@@ -321,8 +342,9 @@ msgstr ""
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr ""
 
@@ -330,24 +352,33 @@ msgstr ""
 msgid "Delete site: %{site_name}"
 msgstr ""
 
+#: src/views/stream/StreamList.vue:81
+msgid "Delete stream: %{stream_name}"
+msgstr ""
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 msgid "Deleted successfully"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 msgid "Deploy successfully"
 msgstr ""
 
@@ -368,7 +399,7 @@ msgstr ""
 msgid "Directive"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr ""
 
@@ -377,6 +408,7 @@ msgid "Directory"
 msgstr ""
 
 #: src/views/domain/DomainList.vue:125
+#: src/views/stream/StreamList.vue:146
 msgid "Disable"
 msgstr ""
 
@@ -387,11 +419,15 @@ msgstr ""
 #: src/views/domain/cert/ChangeCert.vue:48
 #: src/views/domain/DomainEdit.vue:190
 #: src/views/domain/DomainList.vue:36
+#: src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr ""
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr ""
 
@@ -399,7 +435,7 @@ msgstr ""
 msgid "Disk IO"
 msgstr ""
 
-#: src/routes/index.ts:131
+#: src/routes/index.ts:148
 #: src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr ""
@@ -414,6 +450,7 @@ msgid "DNS01"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
 msgstr[0] ""
@@ -427,19 +464,27 @@ msgstr ""
 msgid "Do you want to disable this site?"
 msgstr ""
 
+#: src/views/stream/components/RightSettings.vue:48
+msgid "Do you want to disable this stream?"
+msgstr ""
+
 #: src/views/domain/components/RightSettings.vue:48
 msgid "Do you want to enable this site?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+msgid "Do you want to enable this stream?"
+msgstr ""
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 msgid "Do you want to enable TLS?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 msgid "Do you want to remove this server?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 msgid "Do you want to remove this upstream?"
 msgstr ""
 
@@ -469,52 +514,67 @@ msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 msgid "Duplicate failed"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 msgid "Duplicate successfully"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 msgid "Duplicate to local successfully"
 msgstr ""
 
 #: src/views/domain/DomainEdit.vue:179
+#: src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr ""
 
-#: src/routes/index.ts:93
+#: src/routes/index.ts:110
 #: src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr ""
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr ""
 
+#: src/routes/index.ts:93
+msgid "Edit Stream"
+msgstr ""
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 msgid "Enable"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
@@ -527,10 +587,11 @@ msgid "Enable failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 msgid "Enable successfully"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr ""
 
@@ -538,6 +599,9 @@ msgstr ""
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184
 #: src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175
+#: src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr ""
 
@@ -545,6 +609,9 @@ msgstr ""
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45
 #: src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr ""
 
@@ -552,7 +619,7 @@ msgstr ""
 msgid "Encrypt website with Let's Encrypt"
 msgstr ""
 
-#: src/routes/index.ts:169
+#: src/routes/index.ts:186
 #: src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr ""
@@ -566,7 +633,7 @@ msgstr ""
 msgid "Error"
 msgstr ""
 
-#: src/routes/index.ts:156
+#: src/routes/index.ts:173
 #: src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr ""
@@ -590,11 +657,15 @@ msgstr ""
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr ""
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr ""
 
@@ -603,6 +674,7 @@ msgid "Failed to get certificate information"
 msgstr ""
 
 #: src/views/domain/DomainEdit.vue:138
+#: src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 
@@ -679,7 +751,7 @@ msgstr ""
 msgid "GPT-4-Turbo"
 msgstr ""
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr ""
 
@@ -703,7 +775,7 @@ msgstr ""
 msgid "Import"
 msgstr ""
 
-#: src/routes/index.ts:123
+#: src/routes/index.ts:140
 #: src/views/certificate/CertificateEditor.vue:84
 msgid "Import Certificate"
 msgstr ""
@@ -720,7 +792,7 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/routes/index.ts:220
+#: src/routes/index.ts:237
 #: src/views/other/Install.vue:139
 msgid "Install"
 msgstr ""
@@ -795,7 +867,7 @@ msgstr ""
 msgid "Log"
 msgstr ""
 
-#: src/routes/index.ts:226
+#: src/routes/index.ts:243
 #: src/views/other/Login.vue:147
 msgid "Login"
 msgstr ""
@@ -813,16 +885,21 @@ msgstr ""
 msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort before obtaining the certificate."
 msgstr ""
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr ""
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:60
 #: src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr ""
 
-#: src/routes/index.ts:185
+#: src/routes/index.ts:85
+#: src/views/stream/StreamList.vue:122
+msgid "Manage Streams"
+msgstr ""
+
+#: src/routes/index.ts:202
 #: src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr ""
@@ -845,7 +922,7 @@ msgstr ""
 msgid "Modify"
 msgstr ""
 
-#: src/routes/index.ts:115
+#: src/routes/index.ts:132
 #: src/views/certificate/CertificateEditor.vue:84
 msgid "Modify Certificate"
 msgstr ""
@@ -866,8 +943,12 @@ msgstr ""
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16
+#: src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr ""
 
@@ -906,6 +987,7 @@ msgid "Nginx Access Log Path"
 msgstr ""
 
 #: src/views/domain/DomainEdit.vue:222
+#: src/views/stream/StreamEdit.vue:213
 msgid "Nginx Configuration Parse Error"
 msgstr ""
 
@@ -917,7 +999,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr ""
 
-#: src/routes/index.ts:146
+#: src/routes/index.ts:163
 #: src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr ""
@@ -934,9 +1016,10 @@ msgstr ""
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr ""
 
@@ -948,8 +1031,8 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232
-#: src/routes/index.ts:234
+#: src/routes/index.ts:249
+#: src/routes/index.ts:251
 msgid "Not Found"
 msgstr ""
 
@@ -967,7 +1050,7 @@ msgid "Notification"
 msgstr ""
 
 #: src/components/Notification/Notification.vue:82
-#: src/routes/index.ts:177
+#: src/routes/index.ts:194
 msgid "Notifications"
 msgstr ""
 
@@ -995,10 +1078,13 @@ msgstr ""
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr ""
 
@@ -1026,10 +1112,12 @@ msgid "OS:"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr ""
 
@@ -1069,6 +1157,7 @@ msgid "Please first add credentials in Certification > DNS Credentials, and then
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid "Please input name, this will be used as the filename of the new configuration!"
 msgstr ""
 
@@ -1091,6 +1180,7 @@ msgid "Please note that the unit of time configurations below are all in seconds
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr ""
 
@@ -1099,7 +1189,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:193
+#: src/routes/index.ts:210
 #: src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr ""
@@ -1167,14 +1257,10 @@ msgstr ""
 msgid "Removed successfully"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 msgid "Rename"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 msgid "Renew Certificate"
@@ -1221,8 +1307,9 @@ msgstr ""
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98
 #: src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
 #: src/views/preference/Preference.vue:113
+#: src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr ""
 
@@ -1232,7 +1319,7 @@ msgstr ""
 
 #: src/views/config/ConfigEdit.vue:59
 #: src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr ""
 
@@ -1249,7 +1336,8 @@ msgstr ""
 #: src/views/config/ConfigEdit.vue:57
 #: src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr ""
 
@@ -1272,6 +1360,8 @@ msgstr ""
 #: src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72
 #: src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116
+#: src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr ""
@@ -1305,11 +1395,11 @@ msgstr ""
 msgid "Single Directive"
 msgstr ""
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 msgid "Site Logs"
 msgstr ""
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr ""
 
@@ -1343,6 +1433,7 @@ msgstr ""
 #: src/views/certificate/Certificate.vue:81
 #: src/views/domain/DomainList.vue:25
 #: src/views/environment/Environment.vue:78
+#: src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr ""
 
@@ -1374,7 +1465,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr ""
 
@@ -1383,10 +1474,11 @@ msgid "Table"
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:138
+#: src/routes/index.ts:155
 #: src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr ""
@@ -1427,7 +1519,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid "To make sure the certification auto-renewal can work normally, we need to add a location which can proxy the request from authority to backend, and we need to save this file and reload the Nginx. Are you sure you want to continue?"
 msgstr ""
 
@@ -1443,6 +1535,8 @@ msgstr ""
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44
 #: src/views/environment/Environment.vue:98
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44
 #: src/views/user/User.vue:40
 msgid "Updated at"
 msgstr ""
@@ -1451,7 +1545,7 @@ msgstr ""
 msgid "Updated successfully"
 msgstr ""
 
-#: src/routes/index.ts:212
+#: src/routes/index.ts:229
 #: src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
@@ -1465,6 +1559,10 @@ msgstr ""
 msgid "Upgrading Nginx UI, please wait..."
 msgstr ""
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr ""
@@ -1526,7 +1624,7 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr ""

+ 162 - 72
app/src/language/ru_RU/app.po

@@ -9,26 +9,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "О проекте"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "Журнал доступа"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "Действие"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "Добавить"
 
@@ -41,16 +43,26 @@ msgstr "Добавить директиву ниже"
 msgid "Add Location"
 msgstr "Добавить Location"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "Добавть Сайт"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "Добавть Сайт"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "Обновлено успешно"
+
 #: src/views/certificate/DNSChallenge.vue:94
 #, fuzzy
 msgid "Additional"
 msgstr "Дополнительно"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "Расширенный режим"
 
@@ -82,12 +94,12 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "Вы уверены, что хотите удалить эту директиву?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 #, fuzzy
 msgid "Are you sure you want to delete?"
 msgstr "Вы уверены, что хотите удалить эту директиву?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "Вы уверены, что хотите удалить эту директиву?"
 
@@ -128,6 +140,7 @@ msgstr "Автообновление включено для %{name}"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "Назад"
 
@@ -143,11 +156,12 @@ msgstr "Основная информация"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 #, fuzzy
 msgid "Basic"
 msgstr "Простой режим"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "Простой режим"
 
@@ -172,9 +186,11 @@ msgstr ""
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "Отмена"
 
@@ -191,12 +207,12 @@ msgstr "Сертификат действителен"
 msgid "Certificate Status"
 msgstr "Статус сертификата"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "Статус сертификата"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "Список"
@@ -243,10 +259,10 @@ msgstr "Очистить"
 msgid "Cleared successfully"
 msgstr "Отключено успешно"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "Комментарии"
 
@@ -275,7 +291,7 @@ msgstr "Настроить SSL"
 msgid "Connected"
 msgstr "Подключено"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -293,6 +309,11 @@ msgstr "Нагрузка CPU"
 msgid "CPU:"
 msgstr "CPU:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "Создан в"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "Создать еще"
@@ -318,11 +339,11 @@ msgid "Current Version"
 msgstr "Текущяя версия"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "Пользовательский"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "Доска"
 
@@ -332,8 +353,9 @@ msgstr "База данных (Опционально, по умолчанию:
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "Удалить"
 
@@ -341,6 +363,10 @@ msgstr "Удалить"
 msgid "Delete site: %{site_name}"
 msgstr ""
 
+#: src/views/stream/StreamList.vue:81
+msgid "Delete stream: %{stream_name}"
+msgstr ""
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -348,18 +374,23 @@ msgstr "Отключено успешно"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 #, fuzzy
 msgid "Deploy successfully"
 msgstr "Saved successfully"
@@ -381,7 +412,7 @@ msgstr "Режим разработки"
 msgid "Directive"
 msgstr "Деректива"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "Дерективы"
 
@@ -390,7 +421,7 @@ msgstr "Дерективы"
 msgid "Directory"
 msgstr "Деректива"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 #, fuzzy
 msgid "Disable"
 msgstr "Отключить"
@@ -400,12 +431,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "Не удалось отключить автоматическое продление для %{name}"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "Отключено"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "Отключено успешно"
 
@@ -413,7 +447,7 @@ msgstr "Отключено успешно"
 msgid "Disk IO"
 msgstr "Нагрузка на Диск IO"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr ""
 
@@ -427,6 +461,7 @@ msgid "DNS01"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 #, fuzzy
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
@@ -442,22 +477,32 @@ msgstr "Вы хотите отключить автоматическое обн
 msgid "Do you want to disable this site?"
 msgstr "Вы хотите отключить этот сайт?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "Вы хотите отключить этот сайт?"
+
 #: src/views/domain/components/RightSettings.vue:48
 #, fuzzy
 msgid "Do you want to enable this site?"
 msgstr "Вы хотите включить этот сайт?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "Вы хотите включить этот сайт?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 #, fuzzy
 msgid "Do you want to enable TLS?"
 msgstr "Включить TLS?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 #, fuzzy
 msgid "Do you want to remove this server?"
 msgstr "Вы хотите удалить этот сервер?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "Вы хотите удалить этот сервер?"
@@ -490,56 +535,71 @@ msgstr "Включен пробный режим"
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "Дублировать"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 #, fuzzy
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "Продублированно %{conf_name} в %{node_name}"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 #, fuzzy
 msgid "Duplicate failed"
 msgstr "Дублировать не удалось"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 #, fuzzy
 msgid "Duplicate successfully"
 msgstr "Продублированно"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 #, fuzzy
 msgid "Duplicate to local successfully"
 msgstr "Saved successfully"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "Редактировать %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "Редактировать Конфигурацию"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "Редактировать Сайт"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "Редактировать Сайт"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "Email (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 #, fuzzy
 msgid "Enable"
 msgstr "Включить"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr "Включение %{conf_name} in %{node_name} нипалучилася"
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Включение %{conf_name} in %{node_name} успешно"
 
@@ -552,23 +612,29 @@ msgid "Enable failed"
 msgstr "Включить не удалось"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 #, fuzzy
 msgid "Enable successfully"
 msgstr "Активировано успешно"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "Включить TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "Включено"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "Активировано успешно"
 
@@ -576,7 +642,7 @@ msgstr "Активировано успешно"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Использовать для сайта Let's Encrypt"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr "Окружение"
 
@@ -589,7 +655,7 @@ msgstr "Комментарии"
 msgid "Error"
 msgstr "Ошибка"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "Ошибка логирования"
 
@@ -613,11 +679,15 @@ msgstr "Экспорт"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "Не удалось отключить %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "Не удалось включить %{msg}"
 
@@ -625,7 +695,7 @@ msgstr "Не удалось включить %{msg}"
 msgid "Failed to get certificate information"
 msgstr "Не удалось получить информацию о сертификате"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "Не удалось сохранить, обнаружены синтаксические ошибки в конфигурации."
 
@@ -705,7 +775,7 @@ msgstr ""
 msgid "GPT-4-Turbo"
 msgstr ""
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "Главная"
 
@@ -730,7 +800,7 @@ msgstr ""
 msgid "Import"
 msgstr "Экспорт"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Статус сертификата"
@@ -747,7 +817,7 @@ msgstr "Ошибка первоначального обновления ядр
 msgid "Initialing core upgrader"
 msgstr "Инициализация программы обновления ядра"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "Установить"
 
@@ -830,7 +900,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Логин"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "Логин"
 
@@ -851,15 +921,20 @@ msgstr ""
 "Убедитесь, что вы настроили обратный прокси-сервер для каталога .well-known "
 "на HTTPChallengePort перед получением сертификата»."
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "Конфигурации"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "Сайты"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "Сайты"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "Пользователи"
 
@@ -883,7 +958,7 @@ msgstr "Память и хранилище"
 msgid "Modify"
 msgstr "Изменить"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Статус сертификата"
@@ -904,8 +979,11 @@ msgstr "Одиночная директива"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "Имя"
 
@@ -944,7 +1022,7 @@ msgstr "Журнал"
 msgid "Nginx Access Log Path"
 msgstr "Путь для Nginx Access Log"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 #, fuzzy
 msgid "Nginx Configuration Parse Error"
 msgstr "Ошибка синтаксического анализа конфигурации Nginx"
@@ -957,7 +1035,7 @@ msgstr "Управление Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Путь для Nginx Error Log"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr "Журнал"
 
@@ -975,9 +1053,10 @@ msgstr "Nginx успешно перезапущен"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "Нет"
 
@@ -989,7 +1068,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "Не найден"
 
@@ -1007,7 +1086,7 @@ msgstr "Заметка"
 msgid "Notification"
 msgstr "Сертификат"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "Сертификат"
@@ -1037,10 +1116,13 @@ msgstr ""
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr ""
 
@@ -1069,10 +1151,12 @@ msgid "OS:"
 msgstr "OS:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr ""
 
@@ -1115,6 +1199,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1140,6 +1225,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr ""
 
@@ -1147,7 +1233,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "Настройки"
 
@@ -1217,15 +1303,11 @@ msgstr "Перезагружается nginx"
 msgid "Removed successfully"
 msgstr "Успешно сохранено"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "Имя пользователя"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1276,8 +1358,8 @@ msgstr "Выполняется"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "Сохранить"
 
@@ -1286,7 +1368,7 @@ msgid "Save Directive"
 msgstr "Сохранить директиву"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "Ошибка сохранения %{msg}"
 
@@ -1304,7 +1386,8 @@ msgstr "Успешно сохранено"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "Успешно сохранено"
 
@@ -1325,6 +1408,7 @@ msgstr "Отправлено"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "Ошибка сервера"
@@ -1358,12 +1442,12 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Одиночная Директива"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 #, fuzzy
 msgid "Site Logs"
 msgstr "Логи сайтов"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "Список сайтов"
 
@@ -1400,7 +1484,7 @@ msgid "Stable"
 msgstr "Таблица"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "Статус"
 
@@ -1433,7 +1517,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "Система"
 
@@ -1443,10 +1527,11 @@ msgid "Table"
 msgstr "Таблица"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "Терминал"
 
@@ -1497,7 +1582,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1514,7 +1599,8 @@ msgstr "Тип"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "Обновлено в"
 
@@ -1523,7 +1609,7 @@ msgstr "Обновлено в"
 msgid "Updated successfully"
 msgstr "Обновлено успешно"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "Обновление"
@@ -1537,6 +1623,10 @@ msgstr "Обновление успешно выполнено"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "Обновление Nginx UI, подождите..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "Аптайм:"
@@ -1602,7 +1692,7 @@ msgstr "Запись закрытого ключа сертификата на 
 msgid "Writing certificate to disk"
 msgstr "Запись сертификата на диск"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "Да"

+ 163 - 72
app/src/language/vi_VN/app.po

@@ -9,26 +9,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "Tác giả"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "Log truy cập"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "Hành động"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "Thêm"
 
@@ -41,16 +43,26 @@ msgstr "Thêm Directive"
 msgid "Add Location"
 msgstr "Thêm Location"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "Thêm Website"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "Thêm Website"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "Cập nhật thành công"
+
 #: src/views/certificate/DNSChallenge.vue:94
 #, fuzzy
 msgid "Additional"
 msgstr "Tùy chọn bổ sung"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "Nâng cao"
 
@@ -82,12 +94,12 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "Bạn có chắc chắn muốn xóa lịch sử trò chuyện không ?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 #, fuzzy
 msgid "Are you sure you want to delete?"
 msgstr "Bạn chắc chắn muốn xóa nó "
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "Bạn chắc chắn muốn xoá directive này ?"
 
@@ -128,6 +140,7 @@ msgstr "Đã bật tự động gia hạn SSL cho %{name}"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "Quay lại"
 
@@ -143,11 +156,12 @@ msgstr "Thông tin"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 #, fuzzy
 msgid "Basic"
 msgstr "Cơ bản"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "Cơ bản"
 
@@ -172,9 +186,11 @@ msgstr ""
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "Huỷ"
 
@@ -191,12 +207,12 @@ msgstr "Chứng chỉ SSL hợp lệ"
 msgid "Certificate Status"
 msgstr "Trạng thái chứng chỉ"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "Chứng chỉ"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "Danh sách chứng chỉ"
@@ -243,10 +259,10 @@ msgstr "Xoá"
 msgid "Cleared successfully"
 msgstr "Đã xóa thành công"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "Bình luận"
 
@@ -275,7 +291,7 @@ msgstr "Cấu hình SSL"
 msgid "Connected"
 msgstr "Đã kết nối"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -293,6 +309,11 @@ msgstr "Trạng thái CPU"
 msgid "CPU:"
 msgstr "CPU:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "Ngày tạo"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "Tạo thêm"
@@ -318,11 +339,11 @@ msgid "Current Version"
 msgstr "Phiên bản hiện tại"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "Tuỳ chỉnh"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "Bảng điều khiển"
 
@@ -332,8 +353,9 @@ msgstr "Tên cơ sở dữ liệu (Tuỳ chọn, Mặc định là: database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "Xoá"
 
@@ -341,6 +363,11 @@ msgstr "Xoá"
 msgid "Delete site: %{site_name}"
 msgstr "Xoá trang web: %{site_name}"
 
+#: src/views/stream/StreamList.vue:81
+#, fuzzy
+msgid "Delete stream: %{stream_name}"
+msgstr "Xoá trang web: %{site_name}"
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -348,18 +375,23 @@ msgstr "Đã xoá thành công"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr "Triển khai"
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr "Triển khai %{conf_name} tới %{node_name} thất bại"
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr "Triển khai %{conf_name} tới %{node_name} thành công"
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 #, fuzzy
 msgid "Deploy successfully"
 msgstr "Triển khai thành công"
@@ -381,7 +413,7 @@ msgstr "Chế độ phát triển"
 msgid "Directive"
 msgstr "Directive"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "Directives"
 
@@ -390,7 +422,7 @@ msgstr "Directives"
 msgid "Directory"
 msgstr "Thư mục"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 #, fuzzy
 msgid "Disable"
 msgstr "Tắt"
@@ -400,12 +432,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "Tắt tự động gia hạn SSL cho %{name} thất bại"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "Đã tắt"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "Đã tắt thành công"
 
@@ -413,7 +448,7 @@ msgstr "Đã tắt thành công"
 msgid "Disk IO"
 msgstr "Disk IO"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr "Xác thực DNS"
 
@@ -427,6 +462,7 @@ msgid "DNS01"
 msgstr ""
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 #, fuzzy
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
@@ -442,22 +478,32 @@ msgstr "Bạn muốn tắt tự động gia hạn chứng chỉ SSL ?"
 msgid "Do you want to disable this site?"
 msgstr "Bạn muốn tắt trang web này ?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "Bạn muốn tắt trang web này ?"
+
 #: src/views/domain/components/RightSettings.vue:48
 #, fuzzy
 msgid "Do you want to enable this site?"
 msgstr "Bạn muốn bật trang web này ?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "Bạn muốn bật trang web này ?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 #, fuzzy
 msgid "Do you want to enable TLS?"
 msgstr "Bạn muốn bật TLS ?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 #, fuzzy
 msgid "Do you want to remove this server?"
 msgstr "Bạn muốn xóa máy chủ này ?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "Bạn muốn xóa máy chủ này ?"
@@ -490,56 +536,71 @@ msgstr "Đã bật chế độ Dry run"
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "Nhân bản"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 #, fuzzy
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "Nhân bản %{conf_name} thành %{node_name} thành công"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 #, fuzzy
 msgid "Duplicate failed"
 msgstr "Nhân bản thất bại"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 #, fuzzy
 msgid "Duplicate successfully"
 msgstr "Nhân bản thành công"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 #, fuzzy
 msgid "Duplicate to local successfully"
 msgstr "Đã sao chép thành công vào máy cục bộ"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "Sửa %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "Sửa cấu hình"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "Sửa trang web"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "Sửa trang web"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "Email (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 #, fuzzy
 msgid "Enable"
 msgstr "Đã bật"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr "Không thể bật %{conf_name} trên %{node_name}"
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Đã bật %{conf_name} trên %{node_name}"
 
@@ -552,23 +613,29 @@ msgid "Enable failed"
 msgstr "Bật không thành công"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 #, fuzzy
 msgid "Enable successfully"
 msgstr "Đã bật"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "Bật TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "Đã bật"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "Đã bật"
 
@@ -576,7 +643,7 @@ msgstr "Đã bật"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Bảo mật trang web với Let's Encrypt"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr "Environment"
 
@@ -589,7 +656,7 @@ msgstr "Environments"
 msgid "Error"
 msgstr "Lỗi"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "Log lỗi"
 
@@ -613,11 +680,15 @@ msgstr "Xuất"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "Không thể tắt %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "Không thể bật %{msg}"
 
@@ -625,7 +696,7 @@ msgstr "Không thể bật %{msg}"
 msgid "Failed to get certificate information"
 msgstr "Không thể truy xuất thông tin chứng chỉ"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "Không lưu được, đã phát hiện thấy (các) lỗi cú pháp trong cấu hình."
 
@@ -705,7 +776,7 @@ msgstr ""
 msgid "GPT-4-Turbo"
 msgstr ""
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "Trang chủ"
 
@@ -730,7 +801,7 @@ msgstr ""
 msgid "Import"
 msgstr "Xuất"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Chứng chỉ"
@@ -747,7 +818,7 @@ msgstr "Không thể khởi tạo trình nâng cấp"
 msgid "Initialing core upgrader"
 msgstr "Đang khởi tạo trình nâng cấp"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "Cài đặt"
 
@@ -830,7 +901,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Log"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "Đăng nhập"
 
@@ -851,15 +922,20 @@ msgstr ""
 "Đảm bảo rằng bạn đã định cấu hình proxy ngược (reverse proxy) thư mục .well-"
 "known tới HTTPChallengePort (default: 9180) trước khi ký chứng chỉ SSL."
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "Quản lý cấu hình"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "Quản lý Website"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "Quản lý Website"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "Người dùng"
 
@@ -882,7 +958,7 @@ msgstr "Memory và Storage"
 msgid "Modify"
 msgstr "Sửa"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Sửa chứng chỉ"
@@ -903,8 +979,11 @@ msgstr "Single Directive"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "Tên"
 
@@ -942,7 +1021,7 @@ msgstr ""
 msgid "Nginx Access Log Path"
 msgstr "Vị trí lưu log truy cập (Access log) của Nginx"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 #, fuzzy
 msgid "Nginx Configuration Parse Error"
 msgstr "Lỗi phân tích cú pháp cấu hình Nginx"
@@ -955,7 +1034,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr "Vị trí lưu log lỗi (Error log) của Nginx"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr ""
 
@@ -973,9 +1052,10 @@ msgstr "Restart Nginx thành công"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "Không"
 
@@ -987,7 +1067,7 @@ msgstr ""
 msgid "Not After"
 msgstr "Không phải sau khi"
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "Không tìm thấy"
 
@@ -1005,7 +1085,7 @@ msgstr "Ghi chú"
 msgid "Notification"
 msgstr "Thông báo"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "Thông báo"
@@ -1035,10 +1115,13 @@ msgstr "Ngoại tuyến"
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr ""
 
@@ -1067,10 +1150,12 @@ msgid "OS:"
 msgstr "Hệ điều hành:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr "Ghi đè"
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr "Ghi đè tập tin đã tồn tại"
 
@@ -1116,6 +1201,7 @@ msgstr ""
 "thực DNS, sau đó chọn nhà cung cấp DNS"
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1140,6 +1226,7 @@ msgid ""
 msgstr "Lưu ý đơn vị cấu hình thời gian bên dưới được tính bằng giây."
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr ""
 
@@ -1147,7 +1234,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "Cài đặt"
 
@@ -1217,15 +1304,11 @@ msgstr "Tải lại nginx"
 msgid "Removed successfully"
 msgstr "Xoá thành công"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "Username"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1276,8 +1359,8 @@ msgstr "Running"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "Lưu"
 
@@ -1286,7 +1369,7 @@ msgid "Save Directive"
 msgstr "Lưu Directive"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "Đã xảy ra lỗi khi lưu %{msg}"
 
@@ -1304,7 +1387,8 @@ msgstr "Lưu thành công"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "Lưu thành công"
 
@@ -1325,6 +1409,7 @@ msgstr "Gửi"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "Lỗi máy chủ"
@@ -1359,12 +1444,12 @@ msgstr "Sử dụng HTTP01 để xác thực SSL"
 msgid "Single Directive"
 msgstr "Single Directive"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 #, fuzzy
 msgid "Site Logs"
 msgstr "Logs"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "Danh sách Website"
 
@@ -1396,7 +1481,7 @@ msgid "Stable"
 msgstr "Ổn định"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "Trạng thái"
 
@@ -1429,7 +1514,7 @@ msgstr "Sử dụng Dark theme"
 msgid "Switch to light theme"
 msgstr "Sử dụng Light theme"
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "Thông tin"
 
@@ -1439,10 +1524,11 @@ msgid "Table"
 msgstr "Mục lục"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr "Mục tiêu"
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -1491,7 +1577,7 @@ msgstr "Trường này không được để trống"
 msgid "Title"
 msgstr "Tiêu đề"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1512,7 +1598,8 @@ msgstr "Loại"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "Ngày cập nhật"
 
@@ -1521,7 +1608,7 @@ msgstr "Ngày cập nhật"
 msgid "Updated successfully"
 msgstr "Cập nhật thành công"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "Cập nhật"
@@ -1535,6 +1622,10 @@ msgstr "Cập nhật thành công"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "Đang cập nhật Nginx UI, vui lòng đợi..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "Thời gian hoạt động:"
@@ -1604,7 +1695,7 @@ msgstr "Ghi Private Key vào disk"
 msgid "Writing certificate to disk"
 msgstr "Ghi chứng chỉ vào disk"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "Có"

BIN
app/src/language/zh_CN/app.mo


+ 158 - 75
app/src/language/zh_CN/app.po

@@ -13,26 +13,28 @@ msgstr ""
 "Generated-By: easygettext\n"
 "X-Generator: Poedit 3.4.1\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "关于"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "访问日志"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "操作"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "添加"
 
@@ -45,15 +47,23 @@ msgstr "在下面添加指令"
 msgid "Add Location"
 msgstr "添加 Location"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "添加站点"
 
+#: src/views/stream/StreamList.vue:184
+msgid "Add Stream"
+msgstr "添加 Stream"
+
+#: src/views/stream/StreamList.vue:114
+msgid "Added successfully"
+msgstr "添加成功"
+
 #: src/views/certificate/DNSChallenge.vue:94
 msgid "Additional"
 msgstr "额外选项"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "高级模式"
 
@@ -83,11 +93,11 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "你确定你要清除聊天记录吗?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 msgid "Are you sure you want to delete?"
 msgstr "您确定要删除吗?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "您确定要删除这条指令?"
 
@@ -127,6 +137,7 @@ msgstr "成功启用 %{name} 自动续签"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "返回"
 
@@ -141,10 +152,11 @@ msgstr "基本信息"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 msgid "Basic"
 msgstr "基本"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "基本模式"
 
@@ -168,9 +180,11 @@ msgstr "CADir"
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "取消"
 
@@ -187,11 +201,11 @@ msgstr "此证书有效"
 msgid "Certificate Status"
 msgstr "证书状态"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 msgid "Certificates"
 msgstr "证书"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 msgid "Certificates List"
 msgstr "证书列表"
 
@@ -235,10 +249,10 @@ msgstr "清空"
 msgid "Cleared successfully"
 msgstr "清除成功"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "注释"
 
@@ -266,7 +280,7 @@ msgstr "配置 SSL"
 msgid "Connected"
 msgstr "已连接"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -284,6 +298,10 @@ msgstr "CPU 状态"
 msgid "CPU:"
 msgstr "CPU:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+msgid "Create"
+msgstr "创建"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "再创建一个"
@@ -309,11 +327,11 @@ msgid "Current Version"
 msgstr "当前版本"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "自定义"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "仪表盘"
 
@@ -323,8 +341,9 @@ msgstr "数据库 (可选,默认: database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "删除"
 
@@ -332,24 +351,33 @@ msgstr "删除"
 msgid "Delete site: %{site_name}"
 msgstr "删除站点: %{site_name}"
 
+#: src/views/stream/StreamList.vue:81
+msgid "Delete stream: %{stream_name}"
+msgstr "删除 Stream: %{stream_name}"
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 msgid "Deleted successfully"
 msgstr "删除成功"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr "部署"
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr "部署%{conf_name}到%{node_name}失败"
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr "成功地将%{conf_name}部署到%{node_name}"
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 msgid "Deploy successfully"
 msgstr "部署成功"
 
@@ -370,7 +398,7 @@ msgstr "开发模式"
 msgid "Directive"
 msgstr "指令"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "指令"
 
@@ -378,7 +406,7 @@ msgstr "指令"
 msgid "Directory"
 msgstr "目录"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 msgid "Disable"
 msgstr "禁用"
 
@@ -387,12 +415,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "关闭 %{name} 自动续签失败"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "禁用"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "禁用成功"
 
@@ -400,7 +431,7 @@ msgstr "禁用成功"
 msgid "Disk IO"
 msgstr "磁盘 IO"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr "DNS 凭证"
 
@@ -414,6 +445,7 @@ msgid "DNS01"
 msgstr "DNS01"
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
 msgstr[0] "你想把这个文件部署到远程服务器上吗?"
@@ -426,19 +458,27 @@ msgstr "你想禁用自动更新证书吗?"
 msgid "Do you want to disable this site?"
 msgstr "你想停用这个网站吗?"
 
+#: src/views/stream/components/RightSettings.vue:48
+msgid "Do you want to disable this stream?"
+msgstr "你想停用这个 Stream 吗?"
+
 #: src/views/domain/components/RightSettings.vue:48
 msgid "Do you want to enable this site?"
 msgstr "你想启用这个网站吗?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+msgid "Do you want to enable this stream?"
+msgstr "你想启用这个 Stream 吗?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 msgid "Do you want to enable TLS?"
 msgstr "你想启用TLS吗?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 msgid "Do you want to remove this server?"
 msgstr "你想删除这个服务器吗?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 msgid "Do you want to remove this upstream?"
 msgstr "你想删除这个 Upstream 吗?"
 
@@ -468,51 +508,65 @@ msgstr "试运行模式已启动"
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "复制"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "成功地将%{conf_name}复制到%{node_name}"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 msgid "Duplicate failed"
 msgstr "复制失败"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 msgid "Duplicate successfully"
 msgstr "复制成功"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 msgid "Duplicate to local successfully"
 msgstr "成功复制到本地"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "编辑 %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "编辑配置"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "编辑站点"
 
+#: src/routes/index.ts:93
+msgid "Edit Stream"
+msgstr "编辑 Stream"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "邮箱 (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 msgid "Enable"
 msgstr "启用"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr "在%{node_name}中启用%{conf_name}失败"
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功启用%{node_name}中的%{conf_name}"
 
@@ -525,22 +579,28 @@ msgid "Enable failed"
 msgstr "启用失败"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 msgid "Enable successfully"
 msgstr "启用成功"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "启用 TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "启用"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "启用成功"
 
@@ -548,7 +608,7 @@ msgstr "启用成功"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "用 Let's Encrypt 对网站进行加密"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr "环境"
 
@@ -560,7 +620,7 @@ msgstr "环境"
 msgid "Error"
 msgstr "错误"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "错误日志"
 
@@ -583,11 +643,15 @@ msgstr "导出"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "禁用失败 %{msg}"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "启用失败 %{msg}"
 
@@ -595,7 +659,7 @@ msgstr "启用失败 %{msg}"
 msgid "Failed to get certificate information"
 msgstr "获取证书信息失败"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "保存失败,在配置中检测到语法错误。"
 
@@ -671,7 +735,7 @@ msgstr "GPT-4-32K"
 msgid "GPT-4-Turbo"
 msgstr "GPT-4-Turbo"
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "首页"
 
@@ -695,7 +759,7 @@ msgstr "HTTP01"
 msgid "Import"
 msgstr "导入"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 msgid "Import Certificate"
 msgstr "导入证书"
 
@@ -711,7 +775,7 @@ msgstr "初始化核心升级程序错误"
 msgid "Initialing core upgrader"
 msgstr "初始化核心升级器"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "安装"
 
@@ -785,7 +849,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "日志"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "登录"
 
@@ -805,15 +869,19 @@ msgstr ""
 "在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
 "HTTPChallengePort。"
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "配置管理"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "网站管理"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+msgid "Manage Streams"
+msgstr "管理 Stream"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "用户管理"
 
@@ -835,7 +903,7 @@ msgstr "内存与存储"
 msgid "Modify"
 msgstr "修改"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 msgid "Modify Certificate"
 msgstr "修改证书"
 
@@ -854,8 +922,11 @@ msgstr "多行指令"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "名称"
 
@@ -893,7 +964,7 @@ msgstr "Nginx"
 msgid "Nginx Access Log Path"
 msgstr "Nginx 访问日志路径"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 msgid "Nginx Configuration Parse Error"
 msgstr "Nginx 配置解析错误"
 
@@ -905,7 +976,7 @@ msgstr "控制 Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Nginx 错误日志路径"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr "Nginx 日志"
 
@@ -921,9 +992,10 @@ msgstr "Nginx 重启成功"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "取消"
 
@@ -935,7 +1007,7 @@ msgstr "节点密钥"
 msgid "Not After"
 msgstr "有效期"
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "找不到页面"
 
@@ -952,7 +1024,7 @@ msgstr "注意"
 msgid "Notification"
 msgstr "通知"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 msgid "Notifications"
 msgstr "通知"
 
@@ -980,10 +1052,13 @@ msgstr "离线"
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr "确定"
 
@@ -1011,10 +1086,12 @@ msgid "OS:"
 msgstr "OS:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr "覆盖"
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr "覆盖现有文件"
 
@@ -1059,6 +1136,7 @@ msgstr ""
 "的API。"
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1082,6 +1160,7 @@ msgid ""
 msgstr "请注意,下面的时间单位配置均以秒为单位。"
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr "请至少选择一个节点!"
 
@@ -1089,7 +1168,7 @@ msgstr "请至少选择一个节点!"
 msgid "Pre-release"
 msgstr "预发布"
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "偏好设置"
 
@@ -1156,14 +1235,10 @@ msgstr "正在重载 Nginx"
 msgid "Removed successfully"
 msgstr "删除成功"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 msgid "Rename"
 msgstr "重命名"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr "重新命名 Upstream"
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 msgid "Renew Certificate"
@@ -1209,8 +1284,8 @@ msgstr "运行中"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "保存"
 
@@ -1219,7 +1294,7 @@ msgid "Save Directive"
 msgstr "保存指令"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "保存错误 %{msg}"
 
@@ -1235,7 +1310,8 @@ msgstr "保存成功"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "保存成功"
 
@@ -1256,6 +1332,7 @@ msgstr "上传"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "服务器错误"
@@ -1289,11 +1366,11 @@ msgstr "使用 HTTP01 challenge provider"
 msgid "Single Directive"
 msgstr "单行指令"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 msgid "Site Logs"
 msgstr "站点列表"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "站点列表"
 
@@ -1324,7 +1401,7 @@ msgid "Stable"
 msgstr "稳定"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "状态"
 
@@ -1356,7 +1433,7 @@ msgstr "切换到深色主题"
 msgid "Switch to light theme"
 msgstr "切换到浅色"
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "系统"
 
@@ -1365,10 +1442,11 @@ msgid "Table"
 msgstr "列表"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr "目标"
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "终端"
 
@@ -1414,7 +1492,7 @@ msgstr "该字段不能为空"
 msgid "Title"
 msgstr "标题"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1433,7 +1511,8 @@ msgstr "类型"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "修改时间"
 
@@ -1441,7 +1520,7 @@ msgstr "修改时间"
 msgid "Updated successfully"
 msgstr "更新成功"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "升级"
@@ -1454,6 +1533,10 @@ msgstr "升级成功"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "正在升级Nginx UI,请等待..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr "Upstream 名称"
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "运行时间:"
@@ -1518,7 +1601,7 @@ msgstr "正在将证书私钥写入磁盘"
 msgid "Writing certificate to disk"
 msgstr "正在将证书写入磁盘"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "是的"
@@ -1531,12 +1614,12 @@ msgstr "您使用的是最新版本"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "你可以在这个页面检查Nginx UI的升级。"
 
+#~ msgid "Rename Upstream"
+#~ msgstr "重新命名 Upstream"
+
 #~ msgid "Server"
 #~ msgstr "Server"
 
-#~ msgid "Upstream"
-#~ msgstr "Upstream"
-
 #~ msgid "Leave blank will not change anything."
 #~ msgstr "留空不会有任何变化。"
 

+ 163 - 72
app/src/language/zh_TW/app.po

@@ -14,26 +14,28 @@ msgstr ""
 "Generated-By: easygettext\n"
 "X-Generator: Poedit 3.4.1\n"
 
-#: src/routes/index.ts:208
+#: src/routes/index.ts:225
 msgid "About"
 msgstr "關於"
 
-#: src/routes/index.ts:152 src/views/domain/ngx_conf/LogEntry.vue:78
+#: src/routes/index.ts:169 src/views/domain/ngx_conf/LogEntry.vue:78
 msgid "Access Logs"
 msgstr "存取日誌"
 
 #: src/views/certificate/Certificate.vue:106
 #: src/views/certificate/DNSCredential.vue:32 src/views/config/config.ts:36
 #: src/views/domain/DomainList.vue:50 src/views/environment/Environment.vue:105
-#: src/views/notification/Notification.vue:38 src/views/user/User.vue:46
+#: src/views/notification/Notification.vue:38
+#: src/views/stream/StreamList.vue:50 src/views/user/User.vue:46
 msgid "Action"
 msgstr "操作"
 
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:115
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:141
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:119
-#: src/views/domain/ngx_conf/NgxServer.vue:163
-#: src/views/domain/ngx_conf/NgxUpstream.vue:96
+#: src/views/domain/ngx_conf/NgxServer.vue:170
+#: src/views/domain/ngx_conf/NgxUpstream.vue:153
+#: src/views/stream/StreamList.vue:124
 msgid "Add"
 msgstr "新增"
 
@@ -46,15 +48,25 @@ msgstr "在下方新增指令"
 msgid "Add Location"
 msgstr "新增 Location"
 
-#: src/routes/index.ts:71 src/views/domain/DomainAdd.vue:96
+#: src/routes/index.ts:72 src/views/domain/DomainAdd.vue:96
 msgid "Add Site"
 msgstr "新增網站"
 
+#: src/views/stream/StreamList.vue:184
+#, fuzzy
+msgid "Add Stream"
+msgstr "新增網站"
+
+#: src/views/stream/StreamList.vue:114
+#, fuzzy
+msgid "Added successfully"
+msgstr "更新成功"
+
 #: src/views/certificate/DNSChallenge.vue:94
 msgid "Additional"
 msgstr "其他設定"
 
-#: src/views/domain/DomainEdit.vue:204
+#: src/views/domain/DomainEdit.vue:204 src/views/stream/StreamEdit.vue:195
 msgid "Advance Mode"
 msgstr "進階模式"
 
@@ -85,11 +97,11 @@ msgid "Are you sure you want to clear the record of chat?"
 msgstr "您確定要清除聊天記錄嗎?"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:353
-#: src/views/domain/DomainList.vue:147
+#: src/views/domain/DomainList.vue:147 src/views/stream/StreamList.vue:168
 msgid "Are you sure you want to delete?"
 msgstr "您確定要刪除嗎?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:79
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 msgid "Are you sure you want to remove this directive?"
 msgstr "您確定要刪除這條指令嗎?"
 
@@ -129,6 +141,7 @@ msgstr "已啟用 %{name} 的自動續簽"
 #: src/views/certificate/CertificateEditor.vue:207
 #: src/views/config/Config.vue:75 src/views/config/ConfigEdit.vue:89
 #: src/views/domain/DomainEdit.vue:261 src/views/nginx_log/NginxLog.vue:170
+#: src/views/stream/StreamEdit.vue:251
 msgid "Back"
 msgstr "返回"
 
@@ -143,10 +156,11 @@ msgstr "基本資訊"
 #: src/views/config/ConfigEdit.vue:117
 #: src/views/domain/components/RightSettings.vue:76
 #: src/views/preference/Preference.vue:90
+#: src/views/stream/components/RightSettings.vue:76
 msgid "Basic"
 msgstr "基本"
 
-#: src/views/domain/DomainEdit.vue:207
+#: src/views/domain/DomainEdit.vue:207 src/views/stream/StreamEdit.vue:198
 msgid "Basic Mode"
 msgstr "基本模式"
 
@@ -170,9 +184,11 @@ msgstr "CADir"
 #: src/views/domain/cert/components/ObtainCert.vue:137
 #: src/views/domain/components/Deploy.vue:24
 #: src/views/domain/components/RightSettings.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:49
-#: src/views/domain/ngx_conf/NgxServer.vue:84
-#: src/views/domain/ngx_conf/NgxUpstream.vue:28
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxServer.vue:87
+#: src/views/domain/ngx_conf/NgxUpstream.vue:36
+#: src/views/stream/components/Deploy.vue:24
+#: src/views/stream/components/RightSettings.vue:52
 msgid "Cancel"
 msgstr "取消"
 
@@ -189,12 +205,12 @@ msgstr "此憑證有效"
 msgid "Certificate Status"
 msgstr "憑證狀態"
 
-#: src/routes/index.ts:101 src/views/certificate/Certificate.vue:122
+#: src/routes/index.ts:118 src/views/certificate/Certificate.vue:122
 #, fuzzy
 msgid "Certificates"
 msgstr "憑證狀態"
 
-#: src/routes/index.ts:110
+#: src/routes/index.ts:127
 #, fuzzy
 msgid "Certificates List"
 msgstr "憑證清單"
@@ -240,10 +256,10 @@ msgstr "清除"
 msgid "Cleared successfully"
 msgstr "成功停用"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:107
 #: src/views/domain/ngx_conf/LocationEditor.vue:119
 #: src/views/domain/ngx_conf/LocationEditor.vue:88
-#: src/views/domain/ngx_conf/NgxServer.vue:139
+#: src/views/domain/ngx_conf/NgxServer.vue:142
 msgid "Comments"
 msgstr "備註"
 
@@ -271,7 +287,7 @@ msgstr "設定 SSL"
 msgid "Connected"
 msgstr "已連結"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:102
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:112
 #: src/views/domain/ngx_conf/LocationEditor.vue:100
 #: src/views/domain/ngx_conf/LocationEditor.vue:128
 msgid "Content"
@@ -289,6 +305,11 @@ msgstr "中央處理器狀態"
 msgid "CPU:"
 msgstr "中央處理器:"
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:164
+#, fuzzy
+msgid "Create"
+msgstr "建立時間"
+
 #: src/views/domain/DomainAdd.vue:161
 msgid "Create Another"
 msgstr "再建立一個"
@@ -314,11 +335,11 @@ msgid "Current Version"
 msgstr "目前版本"
 
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:126
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:185
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:188
 msgid "Custom"
 msgstr "自訂"
 
-#: src/routes/index.ts:52
+#: src/routes/index.ts:53
 msgid "Dashboard"
 msgstr "儀表板"
 
@@ -328,8 +349,9 @@ msgstr "資料庫 (可選,預設: database)"
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:360
 #: src/views/domain/DomainList.vue:156
-#: src/views/domain/ngx_conf/NgxServer.vue:114
-#: src/views/domain/ngx_conf/NgxUpstream.vue:77
+#: src/views/domain/ngx_conf/NgxServer.vue:117
+#: src/views/domain/ngx_conf/NgxUpstream.vue:127
+#: src/views/stream/StreamList.vue:177
 msgid "Delete"
 msgstr "刪除"
 
@@ -337,6 +359,11 @@ msgstr "刪除"
 msgid "Delete site: %{site_name}"
 msgstr "刪除網站:%{site_name}"
 
+#: src/views/stream/StreamList.vue:81
+#, fuzzy
+msgid "Delete stream: %{stream_name}"
+msgstr "刪除網站:%{site_name}"
+
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:133
 #, fuzzy
 msgid "Deleted successfully"
@@ -344,18 +371,23 @@ msgstr "成功停用"
 
 #: src/views/domain/components/Deploy.vue:109
 #: src/views/domain/components/RightSettings.vue:94
+#: src/views/stream/components/Deploy.vue:109
+#: src/views/stream/components/RightSettings.vue:94
 msgid "Deploy"
 msgstr "部署"
 
 #: src/views/domain/components/Deploy.vue:66
+#: src/views/stream/components/Deploy.vue:66
 msgid "Deploy %{conf_name} to %{node_name} failed"
 msgstr "部署 %{conf_name} 至 %{node_name} 失敗"
 
 #: src/views/domain/components/Deploy.vue:40
+#: src/views/stream/components/Deploy.vue:40
 msgid "Deploy %{conf_name} to %{node_name} successfully"
 msgstr "成功部署 %{conf_name} 至 %{node_name}"
 
 #: src/views/domain/components/Deploy.vue:38
+#: src/views/stream/components/Deploy.vue:38
 msgid "Deploy successfully"
 msgstr "部署成功"
 
@@ -376,7 +408,7 @@ msgstr "開發模式"
 msgid "Directive"
 msgstr "指令"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:22
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:23
 msgid "Directives"
 msgstr "指令"
 
@@ -385,7 +417,7 @@ msgstr "指令"
 msgid "Directory"
 msgstr "指令"
 
-#: src/views/domain/DomainList.vue:125
+#: src/views/domain/DomainList.vue:125 src/views/stream/StreamList.vue:146
 msgid "Disable"
 msgstr "停用"
 
@@ -394,12 +426,15 @@ msgid "Disable auto-renewal failed for %{name}"
 msgstr "關閉 %{name} 自動續簽失敗"
 
 #: src/views/domain/cert/ChangeCert.vue:48 src/views/domain/DomainEdit.vue:190
-#: src/views/domain/DomainList.vue:36
+#: src/views/domain/DomainList.vue:36 src/views/stream/StreamEdit.vue:181
+#: src/views/stream/StreamList.vue:36
 msgid "Disabled"
 msgstr "停用"
 
 #: src/views/domain/components/RightSettings.vue:39
 #: src/views/domain/DomainList.vue:70
+#: src/views/stream/components/RightSettings.vue:39
+#: src/views/stream/StreamList.vue:70
 msgid "Disabled successfully"
 msgstr "成功停用"
 
@@ -407,7 +442,7 @@ msgstr "成功停用"
 msgid "Disk IO"
 msgstr "磁碟 IO"
 
-#: src/routes/index.ts:131 src/views/certificate/DNSCredential.vue:39
+#: src/routes/index.ts:148 src/views/certificate/DNSCredential.vue:39
 msgid "DNS Credentials"
 msgstr "DNS 認證"
 
@@ -421,6 +456,7 @@ msgid "DNS01"
 msgstr "DNS01"
 
 #: src/views/domain/components/Deploy.vue:19
+#: src/views/stream/components/Deploy.vue:19
 msgid "Do you want to deploy this file to remote server?"
 msgid_plural "Do you want to deploy this file to remote servers?"
 msgstr[0] "您要將此檔案部署至遠端伺服器嗎?"
@@ -433,19 +469,29 @@ msgstr "您要停用自動憑證續訂嗎?"
 msgid "Do you want to disable this site?"
 msgstr "您想停用這個網站嗎?"
 
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to disable this stream?"
+msgstr "您想停用這個網站嗎?"
+
 #: src/views/domain/components/RightSettings.vue:48
 msgid "Do you want to enable this site?"
 msgstr "您要啟用此網站嗎?"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:42
+#: src/views/stream/components/RightSettings.vue:48
+#, fuzzy
+msgid "Do you want to enable this stream?"
+msgstr "您要啟用此網站嗎?"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:45
 msgid "Do you want to enable TLS?"
 msgstr "您想啟用 TLS 嗎?"
 
-#: src/views/domain/ngx_conf/NgxServer.vue:80
+#: src/views/domain/ngx_conf/NgxServer.vue:83
 msgid "Do you want to remove this server?"
 msgstr "您要移除此伺服器嗎?"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:24
+#: src/views/domain/ngx_conf/NgxUpstream.vue:32
 #, fuzzy
 msgid "Do you want to remove this upstream?"
 msgstr "您要移除此伺服器嗎?"
@@ -477,51 +523,66 @@ msgstr "試運轉模式已啟用"
 
 #: src/views/domain/components/SiteDuplicate.vue:128
 #: src/views/domain/DomainList.vue:141
+#: src/views/stream/components/StreamDuplicate.vue:128
+#: src/views/stream/StreamList.vue:162
 msgid "Duplicate"
 msgstr "複製"
 
 #: src/views/domain/components/SiteDuplicate.vue:86
+#: src/views/stream/components/StreamDuplicate.vue:86
 msgid "Duplicate %{conf_name} to %{node_name} successfully"
 msgstr "成功複製 %{conf_name} 到 %{node_name}"
 
 #: src/views/domain/components/SiteDuplicate.vue:92
+#: src/views/stream/components/StreamDuplicate.vue:92
 msgid "Duplicate failed"
 msgstr "複製失敗"
 
 #: src/views/domain/components/SiteDuplicate.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:84
 msgid "Duplicate successfully"
 msgstr "複製成功"
 
 #: src/views/domain/components/SiteDuplicate.vue:66
+#: src/views/stream/components/StreamDuplicate.vue:66
 msgid "Duplicate to local successfully"
 msgstr "成功複製至本機"
 
-#: src/views/domain/DomainEdit.vue:179
+#: src/views/domain/DomainEdit.vue:179 src/views/stream/StreamEdit.vue:170
 msgid "Edit %{n}"
 msgstr "編輯 %{n}"
 
-#: src/routes/index.ts:93 src/views/config/ConfigEdit.vue:83
+#: src/routes/index.ts:110 src/views/config/ConfigEdit.vue:83
 msgid "Edit Configuration"
 msgstr "編輯設定"
 
-#: src/routes/index.ts:75
+#: src/routes/index.ts:76
 msgid "Edit Site"
 msgstr "編輯網站"
 
+#: src/routes/index.ts:93
+#, fuzzy
+msgid "Edit Stream"
+msgstr "編輯網站"
+
 #: src/views/other/Install.vue:93
 msgid "Email (*)"
 msgstr "電子郵件 (*)"
 
 #: src/views/domain/components/Deploy.vue:89
 #: src/views/domain/DomainList.vue:133
+#: src/views/stream/components/Deploy.vue:89
+#: src/views/stream/StreamList.vue:154
 msgid "Enable"
 msgstr "啟用"
 
 #: src/views/domain/components/Deploy.vue:55
+#: src/views/stream/components/Deploy.vue:55
 msgid "Enable %{conf_name} in %{node_name} failed"
 msgstr "在 %{node_name} 啟用 %{conf_name} 失敗"
 
 #: src/views/domain/components/Deploy.vue:49
+#: src/views/stream/components/Deploy.vue:49
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功在 %{node_name} 啟用 %{conf_name}"
 
@@ -534,22 +595,28 @@ msgid "Enable failed"
 msgstr "啟用失敗"
 
 #: src/views/domain/components/Deploy.vue:47
+#: src/views/stream/components/Deploy.vue:47
 msgid "Enable successfully"
 msgstr "啟用成功"
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:174
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:177
 msgid "Enable TLS"
 msgstr "啟用 TLS"
 
 #: src/views/domain/cert/ChangeCert.vue:44
 #: src/views/domain/components/RightSettings.vue:78
 #: src/views/domain/DomainEdit.vue:184 src/views/domain/DomainList.vue:32
+#: src/views/stream/components/RightSettings.vue:78
+#: src/views/stream/StreamEdit.vue:175 src/views/stream/StreamList.vue:32
 msgid "Enabled"
 msgstr "已啟用"
 
 #: src/views/domain/components/RightSettings.vue:30
 #: src/views/domain/components/SiteDuplicate.vue:100
 #: src/views/domain/DomainAdd.vue:45 src/views/domain/DomainList.vue:60
+#: src/views/stream/components/RightSettings.vue:30
+#: src/views/stream/components/StreamDuplicate.vue:100
+#: src/views/stream/StreamList.vue:60
 msgid "Enabled successfully"
 msgstr "成功啟用"
 
@@ -557,7 +624,7 @@ msgstr "成功啟用"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "用 Let's Encrypt 對網站進行加密"
 
-#: src/routes/index.ts:169 src/views/environment/Environment.vue:113
+#: src/routes/index.ts:186 src/views/environment/Environment.vue:113
 msgid "Environment"
 msgstr "環境"
 
@@ -569,7 +636,7 @@ msgstr "環境"
 msgid "Error"
 msgstr "錯誤"
 
-#: src/routes/index.ts:156 src/views/domain/ngx_conf/LogEntry.vue:86
+#: src/routes/index.ts:173 src/views/domain/ngx_conf/LogEntry.vue:86
 msgid "Error Logs"
 msgstr "錯誤日誌"
 
@@ -593,11 +660,15 @@ msgstr "匯出"
 
 #: src/views/domain/components/RightSettings.vue:42
 #: src/views/domain/DomainList.vue:74
+#: src/views/stream/components/RightSettings.vue:42
+#: src/views/stream/StreamList.vue:74
 msgid "Failed to disable %{msg}"
 msgstr "停用 %{msg} 失敗"
 
 #: src/views/domain/components/RightSettings.vue:33
 #: src/views/domain/DomainList.vue:64
+#: src/views/stream/components/RightSettings.vue:33
+#: src/views/stream/StreamList.vue:64
 msgid "Failed to enable %{msg}"
 msgstr "啟用 %{msg} 失敗"
 
@@ -605,7 +676,7 @@ msgstr "啟用 %{msg} 失敗"
 msgid "Failed to get certificate information"
 msgstr "取得憑證資訊失敗"
 
-#: src/views/domain/DomainEdit.vue:138
+#: src/views/domain/DomainEdit.vue:138 src/views/stream/StreamEdit.vue:129
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "儲存失敗,在設定中檢測到語法錯誤。"
 
@@ -682,7 +753,7 @@ msgstr "GPT-4-32K"
 msgid "GPT-4-Turbo"
 msgstr "GPT-4-Turbo"
 
-#: src/routes/index.ts:45
+#: src/routes/index.ts:46
 msgid "Home"
 msgstr "首頁"
 
@@ -707,7 +778,7 @@ msgstr "HTTP01"
 msgid "Import"
 msgstr "匯出"
 
-#: src/routes/index.ts:123 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:140 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Import Certificate"
 msgstr "憑證狀態"
@@ -724,7 +795,7 @@ msgstr "初始化核心升級程式錯誤"
 msgid "Initialing core upgrader"
 msgstr "正在初始化核心升級程式"
 
-#: src/routes/index.ts:220 src/views/other/Install.vue:139
+#: src/routes/index.ts:237 src/views/other/Install.vue:139
 msgid "Install"
 msgstr "安裝"
 
@@ -804,7 +875,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "登入"
 
-#: src/routes/index.ts:226 src/views/other/Login.vue:147
+#: src/routes/index.ts:243 src/views/other/Login.vue:147
 msgid "Login"
 msgstr "登入"
 
@@ -823,15 +894,20 @@ msgid ""
 msgstr ""
 "在取得憑證前,請確保您已將 .well-known 目錄反向代理到 HTTPChallengePort。"
 
-#: src/routes/index.ts:84
+#: src/routes/index.ts:101
 msgid "Manage Configs"
 msgstr "管理設定"
 
-#: src/routes/index.ts:59 src/views/domain/DomainList.vue:105
+#: src/routes/index.ts:60 src/views/domain/DomainList.vue:105
 msgid "Manage Sites"
 msgstr "管理網站"
 
-#: src/routes/index.ts:185 src/views/user/User.vue:53
+#: src/routes/index.ts:85 src/views/stream/StreamList.vue:122
+#, fuzzy
+msgid "Manage Streams"
+msgstr "管理網站"
+
+#: src/routes/index.ts:202 src/views/user/User.vue:53
 msgid "Manage Users"
 msgstr "管理使用者"
 
@@ -854,7 +930,7 @@ msgstr "記憶體與儲存"
 msgid "Modify"
 msgstr "修改"
 
-#: src/routes/index.ts:115 src/views/certificate/CertificateEditor.vue:84
+#: src/routes/index.ts:132 src/views/certificate/CertificateEditor.vue:84
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "憑證狀態"
@@ -874,8 +950,11 @@ msgstr "多行指令"
 #: src/views/domain/components/RightSettings.vue:84
 #: src/views/domain/components/SiteDuplicate.vue:135
 #: src/views/domain/DomainList.vue:16
-#: src/views/domain/ngx_conf/NgxUpstream.vue:108
+#: src/views/domain/ngx_conf/NgxUpstream.vue:176
 #: src/views/environment/Environment.vue:15
+#: src/views/stream/components/RightSettings.vue:84
+#: src/views/stream/components/StreamDuplicate.vue:135
+#: src/views/stream/StreamList.vue:16 src/views/stream/StreamList.vue:188
 msgid "Name"
 msgstr "名稱"
 
@@ -913,7 +992,7 @@ msgstr "Nginx"
 msgid "Nginx Access Log Path"
 msgstr "Nginx 存取日誌路徑"
 
-#: src/views/domain/DomainEdit.vue:222
+#: src/views/domain/DomainEdit.vue:222 src/views/stream/StreamEdit.vue:213
 msgid "Nginx Configuration Parse Error"
 msgstr "Nginx 設定解析錯誤"
 
@@ -925,7 +1004,7 @@ msgstr "Nginx 控制元件"
 msgid "Nginx Error Log Path"
 msgstr "Nginx 錯誤日誌路徑"
 
-#: src/routes/index.ts:146 src/views/nginx_log/NginxLog.vue:145
+#: src/routes/index.ts:163 src/views/nginx_log/NginxLog.vue:145
 msgid "Nginx Log"
 msgstr "Nginx 日誌"
 
@@ -941,9 +1020,10 @@ msgstr "Nginx 重啟成功"
 #: src/components/Notification/Notification.vue:84
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:351
 #: src/views/domain/DomainList.vue:145
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:81
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:91
 #: src/views/domain/ngx_conf/LocationEditor.vue:74
 #: src/views/notification/Notification.vue:71
+#: src/views/stream/StreamList.vue:166
 msgid "No"
 msgstr "取消"
 
@@ -955,7 +1035,7 @@ msgstr "Node Secret"
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:232 src/routes/index.ts:234
+#: src/routes/index.ts:249 src/routes/index.ts:251
 msgid "Not Found"
 msgstr "找不到頁面"
 
@@ -973,7 +1053,7 @@ msgstr "備註"
 msgid "Notification"
 msgstr "憑證"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:177
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:194
 #, fuzzy
 msgid "Notifications"
 msgstr "憑證"
@@ -1002,10 +1082,13 @@ msgstr "離線"
 #: src/views/domain/components/Deploy.vue:23
 #: src/views/domain/components/RightSettings.vue:51
 #: src/views/domain/DomainList.vue:146
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:48
-#: src/views/domain/ngx_conf/NgxServer.vue:83
-#: src/views/domain/ngx_conf/NgxUpstream.vue:27
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:51
+#: src/views/domain/ngx_conf/NgxServer.vue:86
+#: src/views/domain/ngx_conf/NgxUpstream.vue:35
 #: src/views/notification/Notification.vue:72
+#: src/views/stream/components/Deploy.vue:23
+#: src/views/stream/components/RightSettings.vue:51
+#: src/views/stream/StreamList.vue:167
 msgid "OK"
 msgstr "確定"
 
@@ -1033,10 +1116,12 @@ msgid "OS:"
 msgstr "作業系統:"
 
 #: src/views/domain/components/Deploy.vue:93
+#: src/views/stream/components/Deploy.vue:93
 msgid "Overwrite"
 msgstr "覆蓋"
 
 #: src/views/domain/components/Deploy.vue:97
+#: src/views/stream/components/Deploy.vue:97
 msgid "Overwrite exist file"
 msgstr "覆蓋現有檔案"
 
@@ -1082,6 +1167,7 @@ msgstr ""
 "求 DNS 供應商的 API。"
 
 #: src/views/domain/components/SiteDuplicate.vue:40
+#: src/views/stream/components/StreamDuplicate.vue:40
 msgid ""
 "Please input name, this will be used as the filename of the new "
 "configuration!"
@@ -1105,6 +1191,7 @@ msgid ""
 msgstr ""
 
 #: src/views/domain/components/SiteDuplicate.vue:47
+#: src/views/stream/components/StreamDuplicate.vue:47
 msgid "Please select at least one node!"
 msgstr "請至少選擇一個節點!"
 
@@ -1112,7 +1199,7 @@ msgstr "請至少選擇一個節點!"
 msgid "Pre-release"
 msgstr "預先發布"
 
-#: src/routes/index.ts:193 src/views/preference/Preference.vue:85
+#: src/routes/index.ts:210 src/views/preference/Preference.vue:85
 msgid "Preference"
 msgstr "偏好設定"
 
@@ -1180,15 +1267,11 @@ msgstr "正在重新載入 Nginx"
 msgid "Removed successfully"
 msgstr "儲存成功"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:74
+#: src/views/domain/ngx_conf/NgxUpstream.vue:124
 #, fuzzy
 msgid "Rename"
 msgstr "使用者名稱"
 
-#: src/views/domain/ngx_conf/NgxUpstream.vue:103
-msgid "Rename Upstream"
-msgstr ""
-
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1238,8 +1321,8 @@ msgstr "執行中"
 #: src/components/ChatGPT/ChatGPT.vue:259
 #: src/views/certificate/CertificateEditor.vue:214
 #: src/views/config/ConfigEdit.vue:98 src/views/domain/DomainEdit.vue:268
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:111
-#: src/views/preference/Preference.vue:113
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:121
+#: src/views/preference/Preference.vue:113 src/views/stream/StreamEdit.vue:258
 msgid "Save"
 msgstr "儲存"
 
@@ -1248,7 +1331,7 @@ msgid "Save Directive"
 msgstr "儲存指令"
 
 #: src/views/config/ConfigEdit.vue:59 src/views/domain/DomainAdd.vue:53
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:42
 msgid "Save error %{msg}"
 msgstr "儲存錯誤 %{msg}"
 
@@ -1264,7 +1347,8 @@ msgstr "儲存成功"
 
 #: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:41
 #: src/views/domain/DomainEdit.vue:154
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/stream/StreamEdit.vue:145
 msgid "Saved successfully"
 msgstr "儲存成功"
 
@@ -1285,6 +1369,7 @@ msgstr "傳送"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:157
 #: src/views/config/ConfigEdit.vue:42 src/views/domain/DomainList.vue:84
 #: src/views/other/Install.vue:72 src/views/preference/Preference.vue:60
+#: src/views/stream/StreamList.vue:116 src/views/stream/StreamList.vue:84
 #: src/views/system/Upgrade.vue:45
 msgid "Server error"
 msgstr "伺服器錯誤"
@@ -1320,11 +1405,11 @@ msgstr "使用 HTTP01 挑戰提供者"
 msgid "Single Directive"
 msgstr "單一指令"
 
-#: src/routes/index.ts:160
+#: src/routes/index.ts:177
 msgid "Site Logs"
 msgstr "網站日誌"
 
-#: src/routes/index.ts:67
+#: src/routes/index.ts:68
 msgid "Sites List"
 msgstr "網站列表"
 
@@ -1358,7 +1443,7 @@ msgid "Stable"
 msgstr "穩定"
 
 #: src/views/certificate/Certificate.vue:81 src/views/domain/DomainList.vue:25
-#: src/views/environment/Environment.vue:78
+#: src/views/environment/Environment.vue:78 src/views/stream/StreamList.vue:25
 msgid "Status"
 msgstr "狀態"
 
@@ -1391,7 +1476,7 @@ msgstr ""
 msgid "Switch to light theme"
 msgstr ""
 
-#: src/routes/index.ts:201
+#: src/routes/index.ts:218
 msgid "System"
 msgstr "系統"
 
@@ -1400,10 +1485,11 @@ msgid "Table"
 msgstr "表格"
 
 #: src/views/domain/components/SiteDuplicate.vue:142
+#: src/views/stream/components/StreamDuplicate.vue:142
 msgid "Target"
 msgstr "目標"
 
-#: src/routes/index.ts:138 src/views/pty/Terminal.vue:95
+#: src/routes/index.ts:155 src/views/pty/Terminal.vue:95
 msgid "Terminal"
 msgstr "終端機"
 
@@ -1452,7 +1538,7 @@ msgstr "此欄位不應為空"
 msgid "Title"
 msgstr ""
 
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:43
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:46
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "add a location which can proxy the request from authority to backend, and we "
@@ -1471,7 +1557,8 @@ msgstr "類型"
 #: src/views/config/ConfigEdit.vue:123
 #: src/views/domain/components/RightSettings.vue:87
 #: src/views/domain/DomainList.vue:44 src/views/environment/Environment.vue:98
-#: src/views/user/User.vue:40
+#: src/views/stream/components/RightSettings.vue:87
+#: src/views/stream/StreamList.vue:44 src/views/user/User.vue:40
 msgid "Updated at"
 msgstr "更新時間"
 
@@ -1479,7 +1566,7 @@ msgstr "更新時間"
 msgid "Updated successfully"
 msgstr "更新成功"
 
-#: src/routes/index.ts:212 src/views/system/Upgrade.vue:143
+#: src/routes/index.ts:229 src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:235
 msgid "Upgrade"
 msgstr "升級"
@@ -1492,6 +1579,10 @@ msgstr "升級成功"
 msgid "Upgrading Nginx UI, please wait..."
 msgstr "正在升級 Nginx UI,請稍候..."
 
+#: src/views/domain/ngx_conf/NgxUpstream.vue:171
+msgid "Upstream Name"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:177
 msgid "Uptime:"
 msgstr "運作時間:"
@@ -1558,7 +1649,7 @@ msgstr "將憑證私鑰寫入磁碟"
 msgid "Writing certificate to disk"
 msgstr "將憑證寫入磁碟"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:80
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:73
 msgid "Yes"
 msgstr "是的"

+ 17 - 0
app/src/routes/index.ts

@@ -12,6 +12,7 @@ import {
   InfoCircleOutlined,
   SafetyCertificateOutlined,
   SettingOutlined,
+  ShareAltOutlined,
   UserOutlined,
 } from '@ant-design/icons-vue'
 import NProgress from 'nprogress'
@@ -79,6 +80,22 @@ export const routes: Route[] = [
           },
         }],
       },
+      {
+        path: 'streams',
+        name: () => $gettext('Manage Streams'),
+        component: () => import('@/views/stream/StreamList.vue'),
+        meta: {
+          icon: ShareAltOutlined,
+        },
+      },
+      {
+        path: 'stream/:name',
+        name: () => $gettext('Edit Stream'),
+        component: () => import('@/views/stream/StreamEdit.vue'),
+        meta: {
+          hiddenInSidebar: true,
+        },
+      },
       {
         path: 'config',
         name: () => $gettext('Manage Configs'),

+ 1 - 1
app/src/views/domain/DomainAdd.vue

@@ -87,7 +87,7 @@ const ngx_directives = computed(() => {
   return ngx_config.servers[0].directives
 })
 
-provide('save_site_config', save)
+provide('save_config', save)
 provide('ngx_directives', ngx_directives)
 provide('ngx_config', ngx_config)
 </script>

+ 1 - 1
app/src/views/domain/DomainEdit.vue

@@ -157,7 +157,7 @@ const save = async () => {
   })
 }
 
-provide('save_site_config', save)
+provide('save_config', save)
 provide('configText', configText)
 provide('ngx_config', ngx_config)
 provide('history_chatgpt_record', history_chatgpt_record)

+ 3 - 3
app/src/views/domain/cert/components/ObtainCert.vue

@@ -33,7 +33,7 @@ const modalClosable = ref(true)
 
 provide('data', data)
 
-const save_site_config = inject('save_site_config') as () => Promise<void>
+const save_config = inject('save_config') as () => Promise<void>
 const no_server_name = inject('no_server_name') as Ref<boolean>
 const props = inject('props') as Props
 const issuing_cert = inject('issuing_cert') as Ref<boolean>
@@ -53,7 +53,7 @@ const issue_cert = (config_name: string, server_name: string) => {
 async function callback(ssl_certificate: string, ssl_certificate_key: string) {
   directivesMap.value.ssl_certificate[0].params = ssl_certificate
   directivesMap.value.ssl_certificate_key[0].params = ssl_certificate_key
-  await save_site_config()
+  await save_config()
   change_auto_cert(true)
   emit('update:auto_cert', true)
 }
@@ -87,7 +87,7 @@ async function onchange(status: boolean) {
     ngx_config.servers.forEach(v => {
       v.locations = v?.locations?.filter(l => l.path !== '/.well-known/acme-challenge')
     })
-    await save_site_config()
+    await save_config()
     change_auto_cert(status)
   }
 

+ 13 - 9
app/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -9,19 +9,22 @@ import type { CertificateInfo } from '@/api/cert'
 import NgxServer from '@/views/domain/ngx_conf/NgxServer.vue'
 import NgxUpstream from '@/views/domain/ngx_conf/NgxUpstream.vue'
 
-const props = defineProps<{
-  autoCert: boolean
+const props = withDefaults(defineProps<{
+  autoCert?: boolean
   enabled: boolean
-  certInfo?: {
-    [key: number]: CertificateInfo
-  }
-}>()
+  certInfo?: Record<number, CertificateInfo>
+  context?: 'http' | 'stream'
+}>(), {
+  autoCert: false,
+  enabled: false,
+  context: 'http',
+})
 
 const emit = defineEmits(['callback', 'update:autoCert'])
 
 const { $gettext } = useGettext()
 
-const save_site_config = inject('save_site_config') as () => Promise<void>
+const save_config = inject('save_config') as () => Promise<void>
 
 const [modal, ContextHolder] = Modal.useModal()
 
@@ -57,7 +60,7 @@ function confirm_change_tls(status: boolean) {
 
         first.locations.push(...r.locations)
       })
-      await save_site_config()
+      await save_config()
 
       change_tls(status)
     },
@@ -170,7 +173,7 @@ const activeKey = ref(['3'])
   <div>
     <ContextHolder />
     <AFormItem
-      v-if="!support_ssl"
+      v-if="!support_ssl && context === 'http'"
       :label="$gettext('Enable TLS')"
     >
       <ASwitch @change="confirm_change_tls" />
@@ -205,6 +208,7 @@ const activeKey = ref(['3'])
           v-model:auto-cert="autoCertRef"
           :enabled="enabled"
           :cert-info="certInfo"
+          :context="context"
         />
       </ACollapsePanel>
     </ACollapse>

+ 10 - 3
app/src/views/domain/ngx_conf/NgxServer.vue

@@ -12,12 +12,15 @@ import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor.v
 import type { NgxConfig, NgxDirective } from '@/api/ngx'
 import type { CertificateInfo } from '@/api/cert'
 
-defineProps<{
+withDefaults(defineProps<{
   enabled: boolean
   certInfo?: {
     [key: number]: CertificateInfo
   }
-}>()
+  context: 'http' | 'stream'
+}>(), {
+  context: 'http',
+})
 
 const emit = defineEmits(['callback'])
 
@@ -144,9 +147,13 @@ provide('ngx_directives', ngx_directives)
           </template>
           <DirectiveEditor />
           <br>
-          <ConfigTemplate :current-server-index="current_server_index" />
+          <ConfigTemplate
+            v-if="context === 'http'"
+            :current-server-index="current_server_index"
+          />
           <br>
           <LocationEditor
+            v-if="context === 'http'"
             :current-server-index="current_server_index"
             :locations="v.locations"
           />

+ 319 - 0
app/src/views/stream/StreamEdit.vue

@@ -0,0 +1,319 @@
+<script setup lang="ts">
+import { useGettext } from 'vue3-gettext'
+import { message } from 'ant-design-vue'
+import type { Ref } from 'vue'
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+
+import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor.vue'
+import type { NgxConfig } from '@/api/ngx'
+import ngx from '@/api/ngx'
+import config from '@/api/config'
+import RightSettings from '@/views/stream/components/RightSettings.vue'
+import type { ChatComplicationMessage } from '@/api/openai'
+import type { Stream } from '@/api/stream'
+import stream from '@/api/stream'
+
+const { $gettext, interpolate } = useGettext()
+
+const route = useRoute()
+const router = useRouter()
+
+const name = ref(route.params.name.toString())
+
+watch(route, () => {
+  name.value = route.params?.name?.toString() ?? ''
+})
+
+const ngx_config: NgxConfig = reactive({
+  name: '',
+  upstreams: [],
+  servers: [],
+})
+
+const enabled = ref(false)
+const configText = ref('')
+const advance_mode_ref = ref(false)
+const saving = ref(false)
+const filename = ref('')
+const parse_error_status = ref(false)
+const parse_error_message = ref('')
+const data = ref({})
+
+init()
+
+const advance_mode = computed({
+  get() {
+    return advance_mode_ref.value || parse_error_status.value
+  },
+  set(v: boolean) {
+    advance_mode_ref.value = v
+  },
+})
+
+const history_chatgpt_record = ref([]) as Ref<ChatComplicationMessage[]>
+
+function handle_response(r: Stream) {
+  if (r.advanced)
+    advance_mode.value = true
+
+  if (r.advanced)
+    advance_mode.value = true
+
+  parse_error_status.value = false
+  parse_error_message.value = ''
+  filename.value = r.name
+  configText.value = r.config
+  enabled.value = r.enabled
+  history_chatgpt_record.value = r.chatgpt_messages
+  data.value = r
+  Object.assign(ngx_config, r.tokenized)
+}
+
+function init() {
+  if (name.value) {
+    stream.get(name.value).then(r => {
+      handle_response(r)
+    }).catch(handle_parse_error)
+  }
+  else {
+    history_chatgpt_record.value = []
+  }
+}
+
+function handle_parse_error(e: { error?: string; message: string }) {
+  console.error(e)
+  if (e?.error === 'nginx_config_syntax_error') {
+    parse_error_status.value = true
+    parse_error_message.value = e.message
+    config.get(`streams-available/${name.value}`).then(r => {
+      configText.value = r.content
+    })
+  }
+  else {
+    message.error($gettext(e?.message ?? 'Server error'))
+  }
+}
+
+function on_mode_change(advanced: boolean) {
+  stream.advance_mode(name.value, { advanced }).then(() => {
+    advance_mode.value = advanced
+    if (advanced) {
+      build_config()
+    }
+    else {
+      // eslint-disable-next-line promise/no-nesting
+      return ngx.tokenize_config(configText.value).then(r => {
+        Object.assign(ngx_config, r)
+        // eslint-disable-next-line promise/no-nesting
+      }).catch(handle_parse_error)
+    }
+  })
+}
+
+async function build_config() {
+  return ngx.build_config(ngx_config).then(r => {
+    configText.value = r.content
+  })
+}
+
+const save = async () => {
+  saving.value = true
+
+  if (!advance_mode.value) {
+    try {
+      await build_config()
+    }
+    catch (e) {
+      saving.value = false
+      message.error($gettext('Failed to save, syntax error(s) was detected in the configuration.'))
+
+      return
+    }
+  }
+
+  return stream.save(name.value, {
+    name: filename.value || name.value,
+    content: configText.value,
+    overwrite: true,
+  }).then(r => {
+    handle_response(r)
+    router.push({
+      path: `/stream/${filename.value}`,
+      query: route.query,
+    })
+    message.success($gettext('Saved successfully'))
+  }).catch(handle_parse_error).finally(() => {
+    saving.value = false
+  })
+}
+
+provide('save_config', save)
+provide('configText', configText)
+provide('ngx_config', ngx_config)
+provide('history_chatgpt_record', history_chatgpt_record)
+provide('enabled', enabled)
+provide('name', name)
+provide('filename', filename)
+provide('data', data)
+</script>
+
+<template>
+  <ARow :gutter="16">
+    <ACol
+      :xs="24"
+      :sm="24"
+      :md="18"
+    >
+      <ACard :bordered="false">
+        <template #title>
+          <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), { n: name }) }}</span>
+          <ATag
+            v-if="enabled"
+            color="blue"
+          >
+            {{ $gettext('Enabled') }}
+          </ATag>
+          <ATag
+            v-else
+            color="orange"
+          >
+            {{ $gettext('Disabled') }}
+          </ATag>
+        </template>
+        <template #extra>
+          <div class="mode-switch">
+            <div class="switch">
+              <ASwitch
+                size="small"
+                :disabled="parse_error_status"
+                :checked="advance_mode"
+                @change="on_mode_change"
+              />
+            </div>
+            <template v-if="advance_mode">
+              <div>{{ $gettext('Advance Mode') }}</div>
+            </template>
+            <template v-else>
+              <div>{{ $gettext('Basic Mode') }}</div>
+            </template>
+          </div>
+        </template>
+
+        <Transition name="slide-fade">
+          <div
+            v-if="advance_mode"
+            key="advance"
+          >
+            <div
+              v-if="parse_error_status"
+              class="parse-error-alert-wrapper"
+            >
+              <AAlert
+                :message="$gettext('Nginx Configuration Parse Error')"
+                :description="parse_error_message"
+                type="error"
+                show-icon
+              />
+            </div>
+            <div>
+              <CodeEditor v-model:content="configText" />
+            </div>
+          </div>
+
+          <div
+            v-else
+            key="basic"
+            class="domain-edit-container"
+          >
+            <NgxConfigEditor
+              :enabled="enabled"
+              context="stream"
+              @callback="save"
+            />
+          </div>
+        </Transition>
+      </ACard>
+    </ACol>
+
+    <ACol
+      class="col-right"
+      :xs="24"
+      :sm="24"
+      :md="6"
+    >
+      <RightSettings />
+    </ACol>
+
+    <FooterToolBar>
+      <ASpace>
+        <AButton @click="$router.push('/streams')">
+          {{ $gettext('Back') }}
+        </AButton>
+        <AButton
+          type="primary"
+          :loading="saving"
+          @click="save"
+        >
+          {{ $gettext('Save') }}
+        </AButton>
+      </ASpace>
+    </FooterToolBar>
+  </ARow>
+</template>
+
+<style lang="less">
+
+</style>
+
+<style lang="less" scoped>
+.col-right {
+  position: relative;
+}
+
+.ant-card {
+  margin: 10px 0;
+  box-shadow: unset;
+}
+
+.mode-switch {
+  display: flex;
+
+  .switch {
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+  }
+}
+
+.parse-error-alert-wrapper {
+  margin-bottom: 20px;
+}
+
+.domain-edit-container {
+  max-width: 800px;
+  margin: 0 auto;
+}
+
+.slide-fade-enter-active {
+  transition: all .3s ease-in-out;
+}
+
+.slide-fade-leave-active {
+  transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
+}
+
+.slide-fade-enter-from, .slide-fade-enter-to, .slide-fade-leave-to
+  /* .slide-fade-leave-active for below version 2.1.8 */ {
+  transform: translateX(10px);
+  opacity: 0;
+}
+
+.directive-params-wrapper {
+  margin: 10px 0;
+}
+
+.tab-content {
+  padding: 10px;
+}
+</style>

+ 203 - 0
app/src/views/stream/StreamList.vue

@@ -0,0 +1,203 @@
+<script setup lang="tsx">
+import { useGettext } from 'vue3-gettext'
+import { Badge, message } from 'ant-design-vue'
+import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
+import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import stream from '@/api/stream'
+import { input } from '@/components/StdDesign/StdDataEntry'
+import InspectConfig from '@/views/config/InspectConfig.vue'
+import type { Column } from '@/components/StdDesign/types'
+import StreamDuplicate from '@/views/stream/components/StreamDuplicate.vue'
+
+const { $gettext } = useGettext()
+
+const columns: Column[] = [{
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sortable: true,
+  pithy: true,
+  edit: {
+    type: input,
+  },
+  search: true,
+}, {
+  title: () => $gettext('Status'),
+  dataIndex: 'enabled',
+  customRender: (args: customRender) => {
+    const template = []
+    const { text } = args
+    if (text === true || text > 0) {
+      template.push(<Badge status="success"/>)
+      template.push($gettext('Enabled'))
+    }
+    else {
+      template.push(<Badge status="warning"/>)
+      template.push($gettext('Disabled'))
+    }
+
+    return h('div', template)
+  },
+  sortable: true,
+  pithy: true,
+}, {
+  title: () => $gettext('Updated at'),
+  dataIndex: 'modified_at',
+  customRender: datetime,
+  sortable: true,
+  pithy: true,
+}, {
+  title: () => $gettext('Action'),
+  dataIndex: 'action',
+}]
+
+const table = ref()
+
+const inspect_config = ref()
+
+function enable(name: string) {
+  stream.enable(name).then(() => {
+    message.success($gettext('Enabled successfully'))
+    table.value?.get_list()
+    inspect_config.value?.test()
+  }).catch(r => {
+    message.error($gettext('Failed to enable %{msg}', { msg: r.message ?? '' }), 10)
+  })
+}
+
+function disable(name: string) {
+  stream.disable(name).then(() => {
+    message.success($gettext('Disabled successfully'))
+    table.value?.get_list()
+    inspect_config.value?.test()
+  }).catch(r => {
+    message.error($gettext('Failed to disable %{msg}', { msg: r.message ?? '' }))
+  })
+}
+
+function destroy(stream_name: string) {
+  stream.destroy(stream_name).then(() => {
+    table.value.get_list()
+    message.success($gettext('Delete stream: %{stream_name}', { stream_name }))
+    inspect_config.value?.test()
+  }).catch(e => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+
+const showDuplicator = ref(false)
+
+const target = ref('')
+
+function handle_click_duplicate(name: string) {
+  showDuplicator.value = true
+  target.value = name
+}
+
+const route = useRoute()
+
+watch(route, () => {
+  inspect_config.value?.test()
+})
+
+const showAddStream = ref(false)
+const name = ref('')
+function add() {
+  showAddStream.value = true
+  name.value = ''
+}
+
+function handleAddStream() {
+  stream.save(name.value, { name: name.value, content: 'server\t{\n\n}' }).then(() => {
+    showAddStream.value = false
+    table.value?.get_list()
+    message.success($gettext('Added successfully'))
+  }).catch(e => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+</script>
+
+<template>
+  <ACard :title="$gettext('Manage Streams')">
+    <template #extra>
+      <a @click="add">{{ $gettext('Add') }}</a>
+    </template>
+
+    <InspectConfig ref="inspect_config" />
+
+    <StdTable
+      ref="table"
+      :api="stream"
+      :columns="columns"
+      row-key="name"
+      disable-delete
+      @click-edit="r => $router.push({
+        path: `/stream/${r}`,
+      })"
+    >
+      <template #actions="{ record }">
+        <AButton
+          v-if="record.enabled"
+          type="link"
+          size="small"
+          @click="disable(record.name)"
+        >
+          {{ $gettext('Disable') }}
+        </AButton>
+        <AButton
+          v-else
+          type="link"
+          size="small"
+          @click="enable(record.name)"
+        >
+          {{ $gettext('Enable') }}
+        </AButton>
+        <ADivider type="vertical" />
+        <AButton
+          type="link"
+          size="small"
+          @click="handle_click_duplicate(record.name)"
+        >
+          {{ $gettext('Duplicate') }}
+        </AButton>
+        <ADivider type="vertical" />
+        <APopconfirm
+          :cancel-text="$gettext('No')"
+          :ok-text="$gettext('OK')"
+          :title="$gettext('Are you sure you want to delete?')"
+          :disabled="record.enabled"
+          @confirm="destroy(record.name)"
+        >
+          <AButton
+            type="link"
+            size="small"
+            :disabled="record.enabled"
+          >
+            {{ $gettext('Delete') }}
+          </AButton>
+        </APopconfirm>
+      </template>
+    </StdTable>
+    <AModal
+      v-model:open="showAddStream"
+      :title="$gettext('Add Stream')"
+      @ok="handleAddStream"
+    >
+      <AForm layout="vertical">
+        <AFormItem :label="$gettext('Name')">
+          <AInput v-model:value="name" />
+        </AFormItem>
+      </AForm>
+    </AModal>
+    <StreamDuplicate
+      v-model:visible="showDuplicator"
+      :name="target"
+      @duplicated="() => table.get_list()"
+    />
+  </ACard>
+</template>
+
+<style scoped>
+
+</style>

+ 129 - 0
app/src/views/stream/components/Deploy.vue

@@ -0,0 +1,129 @@
+<script setup lang="ts">
+import { useGettext } from 'vue3-gettext'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { Modal, notification } from 'ant-design-vue'
+import type { Ref } from 'vue'
+import stream from '@/api/stream'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+
+const { $gettext, $ngettext } = useGettext()
+
+const node_map = reactive({})
+const target = ref([])
+const overwrite = ref(false)
+const enabled = ref(false)
+const name = inject('name') as Ref<string>
+const [modal, ContextHolder] = Modal.useModal()
+function deploy() {
+  modal.confirm({
+    title: () => $ngettext('Do you want to deploy this file to remote server?',
+      'Do you want to deploy this file to remote servers?', target.value.length),
+    mask: false,
+    centered: true,
+    okText: $gettext('OK'),
+    cancelText: $gettext('Cancel'),
+    onOk() {
+      target.value.forEach(id => {
+        const node_name = node_map[id]
+
+        // get source content
+        stream.get(name.value).then(r => {
+          stream.save(name.value, {
+            name: name.value,
+            content: r.config,
+            overwrite: overwrite.value,
+            // eslint-disable-next-line promise/no-nesting
+          }, { headers: { 'X-Node-ID': id } }).then(async () => {
+            notification.success({
+              message: $gettext('Deploy successfully'),
+              description:
+                $gettext('Deploy %{conf_name} to %{node_name} successfully',
+                  { conf_name: name.value, node_name }),
+            })
+            if (enabled.value) {
+              // eslint-disable-next-line promise/no-nesting
+              stream.enable(name.value).then(() => {
+                notification.success({
+                  message: $gettext('Enable successfully'),
+                  description:
+                    $gettext('Enable %{conf_name} in %{node_name} successfully',
+                      { conf_name: name.value, node_name }),
+                })
+                // eslint-disable-next-line promise/no-nesting
+              }).catch(e => {
+                notification.error({
+                  message: $gettext('Enable %{conf_name} in %{node_name} failed', {
+                    conf_name: name.value,
+                    node_name,
+                  }),
+                  description: $gettext(e?.message ?? 'Server error'),
+                })
+              })
+            }
+            // eslint-disable-next-line promise/no-nesting
+          }).catch(e => {
+            notification.error({
+              message: $gettext('Deploy %{conf_name} to %{node_name} failed', {
+                conf_name: name.value,
+                node_name,
+              }),
+              description: $gettext(e?.message ?? 'Server error'),
+            })
+          })
+        })
+      })
+    },
+  })
+}
+</script>
+
+<template>
+  <ContextHolder />
+  <NodeSelector
+    v-model:target="target"
+    hidden-local
+    :map="node_map"
+  />
+  <div class="node-deploy-control">
+    <ACheckbox v-model:checked="enabled">
+      {{ $gettext('Enable') }}
+    </ACheckbox>
+    <div class="overwrite">
+      <ACheckbox v-model:checked="overwrite">
+        {{ $gettext('Overwrite') }}
+      </ACheckbox>
+      <ATooltip placement="bottom">
+        <template #title>
+          {{ $gettext('Overwrite exist file') }}
+        </template>
+        <InfoCircleOutlined />
+      </ATooltip>
+    </div>
+
+    <AButton
+      :disabled="target.length === 0"
+      type="primary"
+      ghost
+      @click="deploy"
+    >
+      {{ $gettext('Deploy') }}
+    </AButton>
+  </div>
+</template>
+
+<style scoped lang="less">
+.overwrite {
+  margin-right: 15px;
+
+  span {
+    color: #9b9b9b;
+  }
+}
+
+.node-deploy-control {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+  align-items: center;
+}
+</style>

+ 130 - 0
app/src/views/stream/components/RightSettings.vue

@@ -0,0 +1,130 @@
+<script setup lang="ts">
+import { useGettext } from 'vue3-gettext'
+import { Modal, message } from 'ant-design-vue'
+import type { Ref } from 'vue'
+import type { Stream } from '@/api/stream'
+import stream from '@/api/stream'
+import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import { formatDateTime } from '@/lib/helper'
+import Deploy from '@/views/stream/components/Deploy.vue'
+import { useSettingsStore } from '@/pinia'
+import type { ChatComplicationMessage } from '@/api/openai'
+import type { NgxConfig } from '@/api/ngx'
+
+const settings = useSettingsStore()
+const { $gettext } = useGettext()
+const configText = inject('configText') as Ref<string>
+const ngx_config = inject('ngx_config') as Ref<NgxConfig>
+const enabled = inject('enabled') as Ref<boolean>
+const name = inject('name') as Ref<string>
+const history_chatgpt_record = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
+const filename = inject('filename')
+const data = inject('data') as Ref<Stream>
+
+const [modal, ContextHolder] = Modal.useModal()
+
+const active_key = ref(['1', '2', '3'])
+
+function enable() {
+  stream.enable(name.value).then(() => {
+    message.success($gettext('Enabled successfully'))
+    enabled.value = true
+  }).catch(r => {
+    message.error($gettext('Failed to enable %{msg}', { msg: r.message ?? '' }), 10)
+  })
+}
+
+function disable() {
+  stream.disable(name.value).then(() => {
+    message.success($gettext('Disabled successfully'))
+    enabled.value = false
+  }).catch(r => {
+    message.error($gettext('Failed to disable %{msg}', { msg: r.message ?? '' }))
+  })
+}
+
+function on_change_enabled(checked: boolean) {
+  modal.confirm({
+    title: checked ? $gettext('Do you want to enable this stream?') : $gettext('Do you want to disable this stream?'),
+    mask: false,
+    centered: true,
+    okText: $gettext('OK'),
+    cancelText: $gettext('Cancel'),
+    async onOk() {
+      if (checked)
+        enable()
+      else
+        disable()
+    },
+  })
+}
+
+</script>
+
+<template>
+  <ACard
+    class="right-settings"
+    :bordered="false"
+  >
+    <ContextHolder />
+    <ACollapse
+      v-model:activeKey="active_key"
+      ghost
+    >
+      <ACollapsePanel
+        key="1"
+        :header="$gettext('Basic')"
+      >
+        <AFormItem :label="$gettext('Enabled')">
+          <ASwitch
+            :checked="enabled"
+            @change="on_change_enabled"
+          />
+        </AFormItem>
+        <AFormItem :label="$gettext('Name')">
+          <AInput v-model:value="filename" />
+        </AFormItem>
+        <AFormItem :label="$gettext('Updated at')">
+          {{ formatDateTime(data.modified_at) }}
+        </AFormItem>
+      </ACollapsePanel>
+      <ACollapsePanel
+        v-if="!settings.is_remote"
+        key="2"
+        :header="$gettext('Deploy')"
+      >
+        <Deploy />
+      </ACollapsePanel>
+      <ACollapsePanel
+        key="3"
+        header="ChatGPT"
+      >
+        <ChatGPT
+          v-model:history-messages="history_chatgpt_record"
+          :content="configText"
+          :path="ngx_config.file_name"
+        />
+      </ACollapsePanel>
+    </ACollapse>
+  </ACard>
+</template>
+
+<style scoped lang="less">
+.right-settings {
+  position: sticky;
+  top: 78px;
+
+  :deep(.ant-card-body) {
+    max-height: 100vh;
+    overflow-y: scroll;
+  }
+}
+
+:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
+  padding: 0;
+}
+
+:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
+  padding: 0 0 10px 0;
+}
+</style>

+ 156 - 0
app/src/views/stream/components/StreamDuplicate.vue

@@ -0,0 +1,156 @@
+<script setup lang="ts">
+import { useGettext } from 'vue3-gettext'
+import { Form, message, notification } from 'ant-design-vue'
+import gettext from '@/gettext'
+import stream from '@/api/stream'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import { useSettingsStore } from '@/pinia'
+
+const props = defineProps<{
+  visible: boolean
+  name: string
+}>()
+
+const emit = defineEmits(['update:visible', 'duplicated'])
+
+const { $gettext } = useGettext()
+
+const settings = useSettingsStore()
+
+const show = computed({
+  get() {
+    return props.visible
+  },
+  set(v) {
+    emit('update:visible', v)
+  },
+})
+
+interface Model {
+  name: string // site name
+  target: number[] // ids of deploy targets
+}
+
+const modelRef: Model = reactive({ name: '', target: [] })
+
+const rulesRef = reactive({
+  name: [
+    {
+      required: true,
+      message: () => $gettext('Please input name, '
+        + 'this will be used as the filename of the new configuration!'),
+    },
+  ],
+  target: [
+    {
+      required: true,
+      message: () => $gettext('Please select at least one node!'),
+    },
+  ],
+})
+
+const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
+
+const loading = ref(false)
+
+const node_map: Record<number, string> = reactive({})
+
+function onSubmit() {
+  validate().then(async () => {
+    loading.value = true
+
+    modelRef.target.forEach(id => {
+      if (id === 0) {
+        // eslint-disable-next-line promise/no-nesting
+        stream.duplicate(props.name, { name: modelRef.name }).then(() => {
+          message.success($gettext('Duplicate to local successfully'))
+          show.value = false
+          emit('duplicated')
+          // eslint-disable-next-line promise/no-nesting
+        }).catch(e => {
+          message.error($gettext(e?.message ?? 'Server error'))
+        })
+      }
+      else {
+        // get source content
+        // eslint-disable-next-line promise/no-nesting
+        stream.get(props.name).then(r => {
+          stream.save(modelRef.name, {
+            name: modelRef.name,
+            content: r.config,
+            // eslint-disable-next-line promise/no-nesting
+          }, { headers: { 'X-Node-ID': id } }).then(() => {
+            notification.success({
+              message: $gettext('Duplicate successfully'),
+              description:
+                $gettext('Duplicate %{conf_name} to %{node_name} successfully',
+                  { conf_name: props.name, node_name: node_map[id] }),
+            })
+            // eslint-disable-next-line promise/no-nesting
+          }).catch(e => {
+            notification.error({
+              message: $gettext('Duplicate failed'),
+              description: $gettext(e?.message ?? 'Server error'),
+            })
+          })
+          if (r.enabled) {
+            // eslint-disable-next-line promise/no-nesting
+            stream.enable(modelRef.name, { headers: { 'X-Node-ID': id } }).then(() => {
+              notification.success({
+                message: $gettext('Enabled successfully'),
+              })
+            })
+          }
+        })
+      }
+    })
+
+    loading.value = false
+  })
+}
+
+watch(() => props.visible, v => {
+  if (v) {
+    modelRef.name = props.name // default with source name
+    modelRef.target = [0]
+    nextTick(() => clearValidate())
+  }
+})
+
+watch(() => gettext.current, () => {
+  clearValidate()
+})
+</script>
+
+<template>
+  <AModal
+    v-model:open="show"
+    :title="$gettext('Duplicate')"
+    :confirm-loading="loading"
+    :mask="null"
+    @ok="onSubmit"
+  >
+    <AForm layout="vertical">
+      <AFormItem
+        :label="$gettext('Name')"
+        v-bind="validateInfos.name"
+      >
+        <AInput v-model:value="modelRef.name" />
+      </AFormItem>
+      <AFormItem
+        v-if="!settings.is_remote"
+        :label="$gettext('Target')"
+        v-bind="validateInfos.target"
+      >
+        <NodeSelector
+          v-model:target="modelRef.target"
+          :map="node_map"
+        />
+      </AFormItem>
+    </AForm>
+  </AModal>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 20 - 20
model/chatgpt_log.go

@@ -1,35 +1,35 @@
 package model
 
 import (
-    "database/sql/driver"
-    "encoding/json"
-    "fmt"
-    "github.com/pkg/errors"
-    "github.com/sashabaranov/go-openai"
+	"database/sql/driver"
+	"encoding/json"
+	"fmt"
+	"github.com/pkg/errors"
+	"github.com/sashabaranov/go-openai"
 )
 
-type JSON []openai.ChatCompletionMessage
+type ChatGPTCompletionMessages []openai.ChatCompletionMessage
 
-// Scan scan value into Jsonb, implements sql.Scanner interface
-func (j *JSON) Scan(value interface{}) error {
-    bytes, ok := value.([]byte)
-    if !ok {
-        return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
-    }
+// Scan value into Jsonb, implements sql.Scanner interface
+func (j *ChatGPTCompletionMessages) Scan(value interface{}) error {
+	bytes, ok := value.([]byte)
+	if !ok {
+		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
+	}
 
-    result := make([]openai.ChatCompletionMessage, 0)
-    err := json.Unmarshal(bytes, &result)
-    *j = result
+	result := make([]openai.ChatCompletionMessage, 0)
+	err := json.Unmarshal(bytes, &result)
+	*j = result
 
-    return err
+	return err
 }
 
 // Value return json value, implement driver.Valuer interface
-func (j *JSON) Value() (driver.Value, error) {
-    return json.Marshal(*j)
+func (j *ChatGPTCompletionMessages) Value() (driver.Value, error) {
+	return json.Marshal(*j)
 }
 
 type ChatGPTLog struct {
-    Name    string `json:"name"`
-    Content JSON   `json:"content" gorm:"serializer:json"`
+	Name    string                    `json:"name"`
+	Content ChatGPTCompletionMessages `json:"content" gorm:"serializer:json"`
 }

+ 1 - 0
model/model.go

@@ -32,6 +32,7 @@ func GenerateAllModel() []any {
 		Cert{},
 		ChatGPTLog{},
 		Site{},
+		Stream{},
 		DnsCredential{},
 		Environment{},
 		Notification{},

+ 8 - 0
model/stream.go

@@ -0,0 +1,8 @@
+package model
+
+type Stream struct {
+	Model
+	Path            string                    `json:"path"`
+	Advanced        bool                      `json:"advanced"`
+	ChatGPTMessages ChatGPTCompletionMessages `json:"chatgpt_messages" gorm:"serializer:json"`
+}

+ 8 - 0
query/gen.go

@@ -26,6 +26,7 @@ var (
 	Environment   *environment
 	Notification  *notification
 	Site          *site
+	Stream        *stream
 )
 
 func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
@@ -39,6 +40,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	Environment = &Q.Environment
 	Notification = &Q.Notification
 	Site = &Q.Site
+	Stream = &Q.Stream
 }
 
 func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
@@ -53,6 +55,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		Environment:   newEnvironment(db, opts...),
 		Notification:  newNotification(db, opts...),
 		Site:          newSite(db, opts...),
+		Stream:        newStream(db, opts...),
 	}
 }
 
@@ -68,6 +71,7 @@ type Query struct {
 	Environment   environment
 	Notification  notification
 	Site          site
+	Stream        stream
 }
 
 func (q *Query) Available() bool { return q.db != nil }
@@ -84,6 +88,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		Environment:   q.Environment.clone(db),
 		Notification:  q.Notification.clone(db),
 		Site:          q.Site.clone(db),
+		Stream:        q.Stream.clone(db),
 	}
 }
 
@@ -107,6 +112,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		Environment:   q.Environment.replaceDB(db),
 		Notification:  q.Notification.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
+		Stream:        q.Stream.replaceDB(db),
 	}
 }
 
@@ -120,6 +126,7 @@ type queryCtx struct {
 	Environment   *environmentDo
 	Notification  *notificationDo
 	Site          *siteDo
+	Stream        *streamDo
 }
 
 func (q *Query) WithContext(ctx context.Context) *queryCtx {
@@ -133,6 +140,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		Environment:   q.Environment.WithContext(ctx),
 		Notification:  q.Notification.WithContext(ctx),
 		Site:          q.Site.WithContext(ctx),
+		Stream:        q.Stream.WithContext(ctx),
 	}
 }
 

+ 374 - 0
query/streams.gen.go

@@ -0,0 +1,374 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/model"
+)
+
+func newStream(db *gorm.DB, opts ...gen.DOOption) stream {
+	_stream := stream{}
+
+	_stream.streamDo.UseDB(db, opts...)
+	_stream.streamDo.UseModel(&model.Stream{})
+
+	tableName := _stream.streamDo.TableName()
+	_stream.ALL = field.NewAsterisk(tableName)
+	_stream.ID = field.NewInt(tableName, "id")
+	_stream.CreatedAt = field.NewTime(tableName, "created_at")
+	_stream.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_stream.DeletedAt = field.NewField(tableName, "deleted_at")
+	_stream.Path = field.NewString(tableName, "path")
+	_stream.Advanced = field.NewBool(tableName, "advanced")
+	_stream.ChatGPTMessages = field.NewField(tableName, "chat_gpt_messages")
+
+	_stream.fillFieldMap()
+
+	return _stream
+}
+
+type stream struct {
+	streamDo
+
+	ALL             field.Asterisk
+	ID              field.Int
+	CreatedAt       field.Time
+	UpdatedAt       field.Time
+	DeletedAt       field.Field
+	Path            field.String
+	Advanced        field.Bool
+	ChatGPTMessages field.Field
+
+	fieldMap map[string]field.Expr
+}
+
+func (s stream) Table(newTableName string) *stream {
+	s.streamDo.UseTable(newTableName)
+	return s.updateTableName(newTableName)
+}
+
+func (s stream) As(alias string) *stream {
+	s.streamDo.DO = *(s.streamDo.As(alias).(*gen.DO))
+	return s.updateTableName(alias)
+}
+
+func (s *stream) updateTableName(table string) *stream {
+	s.ALL = field.NewAsterisk(table)
+	s.ID = field.NewInt(table, "id")
+	s.CreatedAt = field.NewTime(table, "created_at")
+	s.UpdatedAt = field.NewTime(table, "updated_at")
+	s.DeletedAt = field.NewField(table, "deleted_at")
+	s.Path = field.NewString(table, "path")
+	s.Advanced = field.NewBool(table, "advanced")
+	s.ChatGPTMessages = field.NewField(table, "chat_gpt_messages")
+
+	s.fillFieldMap()
+
+	return s
+}
+
+func (s *stream) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := s.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (s *stream) fillFieldMap() {
+	s.fieldMap = make(map[string]field.Expr, 7)
+	s.fieldMap["id"] = s.ID
+	s.fieldMap["created_at"] = s.CreatedAt
+	s.fieldMap["updated_at"] = s.UpdatedAt
+	s.fieldMap["deleted_at"] = s.DeletedAt
+	s.fieldMap["path"] = s.Path
+	s.fieldMap["advanced"] = s.Advanced
+	s.fieldMap["chat_gpt_messages"] = s.ChatGPTMessages
+}
+
+func (s stream) clone(db *gorm.DB) stream {
+	s.streamDo.ReplaceConnPool(db.Statement.ConnPool)
+	return s
+}
+
+func (s stream) replaceDB(db *gorm.DB) stream {
+	s.streamDo.ReplaceDB(db)
+	return s
+}
+
+type streamDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (s streamDo) FirstByID(id int) (result *model.Stream, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = s.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
+func (s streamDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update streams set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = s.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (s streamDo) Debug() *streamDo {
+	return s.withDO(s.DO.Debug())
+}
+
+func (s streamDo) WithContext(ctx context.Context) *streamDo {
+	return s.withDO(s.DO.WithContext(ctx))
+}
+
+func (s streamDo) ReadDB() *streamDo {
+	return s.Clauses(dbresolver.Read)
+}
+
+func (s streamDo) WriteDB() *streamDo {
+	return s.Clauses(dbresolver.Write)
+}
+
+func (s streamDo) Session(config *gorm.Session) *streamDo {
+	return s.withDO(s.DO.Session(config))
+}
+
+func (s streamDo) Clauses(conds ...clause.Expression) *streamDo {
+	return s.withDO(s.DO.Clauses(conds...))
+}
+
+func (s streamDo) Returning(value interface{}, columns ...string) *streamDo {
+	return s.withDO(s.DO.Returning(value, columns...))
+}
+
+func (s streamDo) Not(conds ...gen.Condition) *streamDo {
+	return s.withDO(s.DO.Not(conds...))
+}
+
+func (s streamDo) Or(conds ...gen.Condition) *streamDo {
+	return s.withDO(s.DO.Or(conds...))
+}
+
+func (s streamDo) Select(conds ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Select(conds...))
+}
+
+func (s streamDo) Where(conds ...gen.Condition) *streamDo {
+	return s.withDO(s.DO.Where(conds...))
+}
+
+func (s streamDo) Order(conds ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Order(conds...))
+}
+
+func (s streamDo) Distinct(cols ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Distinct(cols...))
+}
+
+func (s streamDo) Omit(cols ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Omit(cols...))
+}
+
+func (s streamDo) Join(table schema.Tabler, on ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Join(table, on...))
+}
+
+func (s streamDo) LeftJoin(table schema.Tabler, on ...field.Expr) *streamDo {
+	return s.withDO(s.DO.LeftJoin(table, on...))
+}
+
+func (s streamDo) RightJoin(table schema.Tabler, on ...field.Expr) *streamDo {
+	return s.withDO(s.DO.RightJoin(table, on...))
+}
+
+func (s streamDo) Group(cols ...field.Expr) *streamDo {
+	return s.withDO(s.DO.Group(cols...))
+}
+
+func (s streamDo) Having(conds ...gen.Condition) *streamDo {
+	return s.withDO(s.DO.Having(conds...))
+}
+
+func (s streamDo) Limit(limit int) *streamDo {
+	return s.withDO(s.DO.Limit(limit))
+}
+
+func (s streamDo) Offset(offset int) *streamDo {
+	return s.withDO(s.DO.Offset(offset))
+}
+
+func (s streamDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *streamDo {
+	return s.withDO(s.DO.Scopes(funcs...))
+}
+
+func (s streamDo) Unscoped() *streamDo {
+	return s.withDO(s.DO.Unscoped())
+}
+
+func (s streamDo) Create(values ...*model.Stream) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return s.DO.Create(values)
+}
+
+func (s streamDo) CreateInBatches(values []*model.Stream, batchSize int) error {
+	return s.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (s streamDo) Save(values ...*model.Stream) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return s.DO.Save(values)
+}
+
+func (s streamDo) First() (*model.Stream, error) {
+	if result, err := s.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Stream), nil
+	}
+}
+
+func (s streamDo) Take() (*model.Stream, error) {
+	if result, err := s.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Stream), nil
+	}
+}
+
+func (s streamDo) Last() (*model.Stream, error) {
+	if result, err := s.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Stream), nil
+	}
+}
+
+func (s streamDo) Find() ([]*model.Stream, error) {
+	result, err := s.DO.Find()
+	return result.([]*model.Stream), err
+}
+
+func (s streamDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Stream, err error) {
+	buf := make([]*model.Stream, 0, batchSize)
+	err = s.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (s streamDo) FindInBatches(result *[]*model.Stream, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return s.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (s streamDo) Attrs(attrs ...field.AssignExpr) *streamDo {
+	return s.withDO(s.DO.Attrs(attrs...))
+}
+
+func (s streamDo) Assign(attrs ...field.AssignExpr) *streamDo {
+	return s.withDO(s.DO.Assign(attrs...))
+}
+
+func (s streamDo) Joins(fields ...field.RelationField) *streamDo {
+	for _, _f := range fields {
+		s = *s.withDO(s.DO.Joins(_f))
+	}
+	return &s
+}
+
+func (s streamDo) Preload(fields ...field.RelationField) *streamDo {
+	for _, _f := range fields {
+		s = *s.withDO(s.DO.Preload(_f))
+	}
+	return &s
+}
+
+func (s streamDo) FirstOrInit() (*model.Stream, error) {
+	if result, err := s.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Stream), nil
+	}
+}
+
+func (s streamDo) FirstOrCreate() (*model.Stream, error) {
+	if result, err := s.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Stream), nil
+	}
+}
+
+func (s streamDo) FindByPage(offset int, limit int) (result []*model.Stream, count int64, err error) {
+	result, err = s.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = s.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (s streamDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = s.Count()
+	if err != nil {
+		return
+	}
+
+	err = s.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (s streamDo) Scan(result interface{}) (err error) {
+	return s.DO.Scan(result)
+}
+
+func (s streamDo) Delete(models ...*model.Stream) (result gen.ResultInfo, err error) {
+	return s.DO.Delete(models)
+}
+
+func (s *streamDo) withDO(do gen.Dao) *streamDo {
+	s.DO = *do.(*gen.DO)
+	return s
+}

+ 2 - 0
router/routers.go

@@ -9,6 +9,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/api/notification"
 	"github.com/0xJacky/Nginx-UI/api/openai"
 	"github.com/0xJacky/Nginx-UI/api/sites"
+	"github.com/0xJacky/Nginx-UI/api/streams"
 	"github.com/0xJacky/Nginx-UI/api/system"
 	"github.com/0xJacky/Nginx-UI/api/template"
 	"github.com/0xJacky/Nginx-UI/api/terminal"
@@ -49,6 +50,7 @@ func InitRouter() *gin.Engine {
 			user.InitManageUserRouter(g)
 			nginx.InitRouter(g)
 			sites.InitRouter(g)
+			streams.InitRouter(g)
 			config.InitRouter(g)
 			template.InitRouter(g)
 			certificate.InitCertificateRouter(g)