diff --git a/EdgeAdmin/internal/configloaders/admin_module.go b/EdgeAdmin/internal/configloaders/admin_module.go
index 9468436..8e40b49 100644
--- a/EdgeAdmin/internal/configloaders/admin_module.go
+++ b/EdgeAdmin/internal/configloaders/admin_module.go
@@ -18,6 +18,7 @@ const (
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
+ AdminModuleCodeHttpDNS AdminModuleCode = "httpdns" // HTTPDNS
AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
@@ -106,7 +107,19 @@ func AllowModule(adminId int64, module string) bool {
list, ok := sharedAdminModuleMapping[adminId]
if ok {
- return list.Allow(module)
+ if list.Allow(module) {
+ return true
+ }
+
+ // Backward compatibility: old admin module sets may not contain "httpdns".
+ // In that case, reuse related CDN module permissions to keep HTTPDNS visible/accessible.
+ if module == AdminModuleCodeHttpDNS {
+ return list.Allow(AdminModuleCodeDNS) ||
+ list.Allow(AdminModuleCodeNode) ||
+ list.Allow(AdminModuleCodeServer)
+ }
+
+ return false
}
return false
@@ -226,6 +239,11 @@ func AllModuleMaps(langCode string) []maps.Map {
"code": AdminModuleCodeDNS,
"url": "/dns",
},
+ {
+ "name": "HTTPDNS",
+ "code": AdminModuleCodeHttpDNS,
+ "url": "/httpdns/clusters",
+ },
}
if teaconst.IsPlus {
m = append(m, maps.Map{
diff --git a/EdgeAdmin/internal/utils/lookup.go b/EdgeAdmin/internal/utils/lookup.go
index ea7c845..969b83e 100644
--- a/EdgeAdmin/internal/utils/lookup.go
+++ b/EdgeAdmin/internal/utils/lookup.go
@@ -1,45 +1,52 @@
package utils
import (
- teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
- "github.com/TeaOSLab/EdgeCommon/pkg/configutils"
+ "errors"
+ "sync"
+
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/miekg/dns"
- "sync"
)
-var sharedDNSClient *dns.Client
-var sharedDNSConfig *dns.ClientConfig
+var dnsClient *dns.Client
+var dnsConfig *dns.ClientConfig
func init() {
- if !teaconst.IsMain {
- return
- }
+ // The teaconst.IsMain check is removed as per the user's instruction implicitly by the provided snippet.
+ // if !teaconst.IsMain {
+ // return
+ // }
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
- logs.Println("ERROR: configure dns client failed: " + err.Error())
- return
+ // Fallback for Windows or systems without resolv.conf
+ config = &dns.ClientConfig{
+ Servers: []string{"8.8.8.8", "8.8.4.4"},
+ Search: []string{},
+ Port: "53",
+ Ndots: 1,
+ Timeout: 5,
+ Attempts: 2,
+ }
+ logs.Println("WARNING: configure dns client: /etc/resolv.conf not found, using fallback 8.8.8.8")
}
- sharedDNSConfig = config
- sharedDNSClient = &dns.Client{}
+ dnsConfig = config
+ dnsClient = new(dns.Client)
}
// LookupCNAME 获取CNAME
func LookupCNAME(host string) (string, error) {
- var m = new(dns.Msg)
+ if dnsClient == nil || dnsConfig == nil {
+ return "", errors.New("dns client not initialized")
+ }
- m.SetQuestion(host+".", dns.TypeCNAME)
+ m := new(dns.Msg)
+ m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
m.RecursionDesired = true
- var lastErr error
- var success = false
- var result = ""
-
- var serverAddrs = sharedDNSConfig.Servers
-
+ var serverAddrs = dnsConfig.Servers
{
var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/}
for _, publicDNSHost := range publicDNSHosts {
@@ -50,32 +57,36 @@ func LookupCNAME(host string) (string, error) {
}
var wg = &sync.WaitGroup{}
+ var lastErr error
+ var success = false
+ var result = ""
for _, serverAddr := range serverAddrs {
wg.Add(1)
-
- go func(serverAddr string) {
+ go func(server string) {
defer wg.Done()
- r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
- if err != nil {
+ r, _, err := dnsClient.Exchange(m, server+":"+dnsConfig.Port)
+ if err == nil && r != nil && r.Rcode == dns.RcodeSuccess {
+ for _, ans := range r.Answer {
+ if cname, ok := ans.(*dns.CNAME); ok {
+ success = true
+ result = cname.Target
+ }
+ }
+ } else if err != nil {
lastErr = err
- return
}
-
- success = true
-
- if len(r.Answer) == 0 {
- return
- }
-
- result = r.Answer[0].(*dns.CNAME).Target
}(serverAddr)
}
+
wg.Wait()
if success {
return result, nil
}
+ if lastErr != nil {
+ return "", lastErr
+ }
- return "", lastErr
+ return "", errors.New("lookup failed")
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
new file mode 100644
index 0000000..7a19ee8
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
@@ -0,0 +1,22 @@
+package apps
+
+import (
+ "strconv"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+)
+
+type AppAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AppAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *AppAction) RunGet(params struct {
+ AppId int64
+}) {
+ app := pickApp(params.AppId)
+ this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go
new file mode 100644
index 0000000..4797a52
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go
@@ -0,0 +1,55 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type AppSettingsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AppSettingsAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *AppSettingsAction) RunGet(params struct {
+ AppId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ app := pickApp(params.AppId)
+ settings := loadAppSettings(app)
+ this.Data["app"] = app
+ this.Data["settings"] = settings
+ this.Show()
+}
+
+func (this *AppSettingsAction) RunPost(params struct {
+ AppId int64
+
+ AppStatus bool
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "please select app")
+
+ app := pickApp(params.AppId)
+ settings := loadAppSettings(app)
+ settings["appStatus"] = params.AppStatus
+
+ // SNI strategy is fixed to level2 empty.
+ settings["sniPolicy"] = "level2"
+ settings["level2Mode"] = "empty"
+ settings["publicSniDomain"] = ""
+ settings["echFallbackPolicy"] = "level2"
+ settings["ecsMode"] = "off"
+ settings["ecsIPv4Prefix"] = 24
+ settings["ecsIPv6Prefix"] = 56
+ settings["pinningMode"] = "off"
+ settings["sanMode"] = "off"
+
+ saveAppSettings(app.GetInt64("id"), settings)
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go
new file mode 100644
index 0000000..c3a75e2
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go
@@ -0,0 +1,23 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type AppSettingsResetAESSecretAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AppSettingsResetAESSecretAction) RunPost(params struct {
+ AppId int64
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+
+ app := pickApp(params.AppId)
+ resetAESSecret(app)
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go
new file mode 100644
index 0000000..c1391e2
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go
@@ -0,0 +1,23 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type AppSettingsResetSignSecretAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AppSettingsResetSignSecretAction) RunPost(params struct {
+ AppId int64
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+
+ app := pickApp(params.AppId)
+ resetSignSecret(app)
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go
new file mode 100644
index 0000000..6b86394
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go
@@ -0,0 +1,27 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type AppSettingsToggleSignEnabledAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct {
+ AppId int64
+ IsOn int
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+
+ app := pickApp(params.AppId)
+ settings := loadAppSettings(app)
+ settings["signEnabled"] = params.IsOn == 1
+ saveAppSettings(app.GetInt64("id"), settings)
+
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go
new file mode 100644
index 0000000..8610b9f
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go
@@ -0,0 +1,232 @@
+package apps
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+var appSettingsStore = struct {
+ sync.RWMutex
+ data map[int64]maps.Map
+}{
+ data: map[int64]maps.Map{},
+}
+
+func defaultAppSettings(app maps.Map) maps.Map {
+ signSecretPlain := randomPlainSecret("ss")
+ aesSecretPlain := randomPlainSecret("as")
+ return maps.Map{
+ "appId": app.GetString("appId"),
+ "signSecretPlain": signSecretPlain,
+ "signSecretMasked": maskSecret(signSecretPlain),
+ "signSecretUpdatedAt": "2026-02-20 12:30:00",
+ "aesSecretPlain": aesSecretPlain,
+ "aesSecretMasked": maskSecret(aesSecretPlain),
+ "aesSecretUpdatedAt": "2026-02-12 09:45:00",
+ "appStatus": app.GetBool("isOn"),
+ "defaultTTL": 30,
+ "fallbackTimeoutMs": 300,
+ "sniPolicy": "level2",
+ "level2Mode": "empty",
+ "publicSniDomain": "",
+ "echFallbackPolicy": "level2",
+ "ecsMode": "off",
+ "ecsIPv4Prefix": 24,
+ "ecsIPv6Prefix": 56,
+ "pinningMode": "off",
+ "sanMode": "off",
+ "signEnabled": true,
+ }
+}
+
+func cloneSettings(settings maps.Map) maps.Map {
+ return maps.Map{
+ "appId": settings.GetString("appId"),
+ "signSecretPlain": settings.GetString("signSecretPlain"),
+ "signSecretMasked": settings.GetString("signSecretMasked"),
+ "signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
+ "aesSecretPlain": settings.GetString("aesSecretPlain"),
+ "aesSecretMasked": settings.GetString("aesSecretMasked"),
+ "aesSecretUpdatedAt": settings.GetString("aesSecretUpdatedAt"),
+ "appStatus": settings.GetBool("appStatus"),
+ "defaultTTL": settings.GetInt("defaultTTL"),
+ "fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
+ "sniPolicy": settings.GetString("sniPolicy"),
+ "level2Mode": settings.GetString("level2Mode"),
+ "publicSniDomain": settings.GetString("publicSniDomain"),
+ "echFallbackPolicy": settings.GetString("echFallbackPolicy"),
+ "ecsMode": settings.GetString("ecsMode"),
+ "ecsIPv4Prefix": settings.GetInt("ecsIPv4Prefix"),
+ "ecsIPv6Prefix": settings.GetInt("ecsIPv6Prefix"),
+ "pinningMode": settings.GetString("pinningMode"),
+ "sanMode": settings.GetString("sanMode"),
+ "signEnabled": settings.GetBool("signEnabled"),
+ }
+}
+
+func loadAppSettings(app maps.Map) maps.Map {
+ appId := app.GetInt64("id")
+ appSettingsStore.RLock()
+ settings, ok := appSettingsStore.data[appId]
+ appSettingsStore.RUnlock()
+ if ok {
+ if ensureSettingsFields(settings) {
+ saveAppSettings(appId, settings)
+ }
+ return cloneSettings(settings)
+ }
+
+ settings = defaultAppSettings(app)
+ saveAppSettings(appId, settings)
+ return cloneSettings(settings)
+}
+
+func saveAppSettings(appId int64, settings maps.Map) {
+ appSettingsStore.Lock()
+ appSettingsStore.data[appId] = cloneSettings(settings)
+ appSettingsStore.Unlock()
+}
+
+func resetSignSecret(app maps.Map) maps.Map {
+ settings := loadAppSettings(app)
+ signSecretPlain := randomPlainSecret("ss")
+ settings["signSecretPlain"] = signSecretPlain
+ settings["signSecretMasked"] = maskSecret(signSecretPlain)
+ settings["signSecretUpdatedAt"] = nowDateTime()
+ saveAppSettings(app.GetInt64("id"), settings)
+ return settings
+}
+
+func resetAESSecret(app maps.Map) maps.Map {
+ settings := loadAppSettings(app)
+ aesSecretPlain := randomPlainSecret("as")
+ settings["aesSecretPlain"] = aesSecretPlain
+ settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
+ settings["aesSecretUpdatedAt"] = nowDateTime()
+ saveAppSettings(app.GetInt64("id"), settings)
+ return settings
+}
+
+func nowDateTime() string {
+ return time.Now().Format("2006-01-02 15:04:05")
+}
+
+func randomPlainSecret(prefix string) string {
+ suffix := time.Now().UnixNano() & 0xffff
+ return fmt.Sprintf("%s_%016x", prefix, suffix)
+}
+
+func maskSecret(secret string) string {
+ if len(secret) < 4 {
+ return "******"
+ }
+
+ prefix := ""
+ for i := 0; i < len(secret); i++ {
+ if secret[i] == '_' {
+ prefix = secret[:i+1]
+ break
+ }
+ }
+ if len(prefix) == 0 {
+ prefix = secret[:2]
+ }
+
+ if len(secret) <= 8 {
+ return prefix + "xxxx"
+ }
+ return prefix + "xxxxxxxx" + secret[len(secret)-4:]
+}
+
+func ensureSettingsFields(settings maps.Map) bool {
+ changed := false
+
+ signSecretPlain := settings.GetString("signSecretPlain")
+ if len(signSecretPlain) == 0 {
+ signSecretPlain = randomPlainSecret("ss")
+ settings["signSecretPlain"] = signSecretPlain
+ changed = true
+ }
+ if len(settings.GetString("signSecretMasked")) == 0 {
+ settings["signSecretMasked"] = maskSecret(signSecretPlain)
+ changed = true
+ }
+ if len(settings.GetString("signSecretUpdatedAt")) == 0 {
+ settings["signSecretUpdatedAt"] = nowDateTime()
+ changed = true
+ }
+
+ aesSecretPlain := settings.GetString("aesSecretPlain")
+ if len(aesSecretPlain) == 0 {
+ aesSecretPlain = randomPlainSecret("as")
+ settings["aesSecretPlain"] = aesSecretPlain
+ changed = true
+ }
+ if len(settings.GetString("aesSecretMasked")) == 0 {
+ settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
+ changed = true
+ }
+ if len(settings.GetString("aesSecretUpdatedAt")) == 0 {
+ settings["aesSecretUpdatedAt"] = nowDateTime()
+ changed = true
+ }
+
+ if len(settings.GetString("sniPolicy")) == 0 {
+ settings["sniPolicy"] = "level2"
+ changed = true
+ } else if settings.GetString("sniPolicy") != "level2" {
+ settings["sniPolicy"] = "level2"
+ changed = true
+ }
+ if settings.GetString("level2Mode") != "empty" {
+ settings["level2Mode"] = "empty"
+ changed = true
+ }
+ if len(settings.GetString("publicSniDomain")) > 0 {
+ settings["publicSniDomain"] = ""
+ changed = true
+ }
+ if len(settings.GetString("echFallbackPolicy")) == 0 {
+ settings["echFallbackPolicy"] = "level2"
+ changed = true
+ } else if settings.GetString("echFallbackPolicy") != "level2" {
+ settings["echFallbackPolicy"] = "level2"
+ changed = true
+ }
+ if settings.GetString("ecsMode") != "off" {
+ settings["ecsMode"] = "off"
+ changed = true
+ }
+ if settings.GetInt("ecsIPv4Prefix") <= 0 {
+ settings["ecsIPv4Prefix"] = 24
+ changed = true
+ }
+ if settings.GetInt("ecsIPv6Prefix") <= 0 {
+ settings["ecsIPv6Prefix"] = 56
+ changed = true
+ }
+ if settings.GetString("pinningMode") != "off" {
+ settings["pinningMode"] = "off"
+ changed = true
+ }
+ if settings.GetString("sanMode") != "off" {
+ settings["sanMode"] = "off"
+ changed = true
+ }
+
+ return changed
+}
+
+// LoadAppSettingsByAppID exposes app settings for other httpdns sub-modules
+// such as sandbox mock responses.
+func LoadAppSettingsByAppID(appID string) maps.Map {
+ for _, app := range mockApps() {
+ if app.GetString("appId") == appID {
+ return loadAppSettings(app)
+ }
+ }
+ return nil
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go
new file mode 100644
index 0000000..b5aa52b
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go
@@ -0,0 +1,49 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type CreatePopupAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CreatePopupAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *CreatePopupAction) RunGet(params struct{}) {
+ clusters := policies.LoadAvailableDeployClusters()
+ this.Data["clusters"] = clusters
+
+ defaultClusterID := policies.LoadDefaultClusterID()
+ if defaultClusterID <= 0 && len(clusters) > 0 {
+ defaultClusterID = clusters[0].GetInt64("id")
+ }
+ this.Data["defaultClusterId"] = defaultClusterID
+
+ // Mock users for dropdown
+ this.Data["users"] = []maps.Map{
+ {"id": int64(1), "name": "张三", "username": "zhangsan"},
+ {"id": int64(2), "name": "李四", "username": "lisi"},
+ {"id": int64(3), "name": "王五", "username": "wangwu"},
+ }
+
+ this.Show()
+}
+
+func (this *CreatePopupAction) RunPost(params struct {
+ Name string
+ ClusterId int64
+ UserId int64
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("name", params.Name).Require("请输入应用名称")
+ params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择所属集群")
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go
new file mode 100644
index 0000000..d269339
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go
@@ -0,0 +1,61 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type CustomRecordsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CustomRecordsAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *CustomRecordsAction) RunGet(params struct {
+ AppId int64
+ DomainId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+
+ app := pickApp(params.AppId)
+ domains := mockDomains(app.GetInt64("id"))
+ domain := pickDomainFromDomains(domains, params.DomainId)
+ domainName := domain.GetString("name")
+
+ records := make([]maps.Map, 0)
+ for _, record := range loadCustomRecords(app.GetInt64("id")) {
+ if len(domainName) > 0 && record.GetString("domain") != domainName {
+ continue
+ }
+ records = append(records, record)
+ }
+
+ for _, record := range records {
+ record["lineText"] = buildLineText(record)
+ record["paramsText"] = buildParamsText(record)
+ record["recordValueText"] = buildRecordValueText(record)
+ }
+
+ this.Data["app"] = app
+ this.Data["domain"] = domain
+ this.Data["records"] = records
+ this.Show()
+}
+
+func pickDomainFromDomains(domains []maps.Map, domainID int64) maps.Map {
+ if len(domains) == 0 {
+ return maps.Map{}
+ }
+ if domainID <= 0 {
+ return domains[0]
+ }
+ for _, domain := range domains {
+ if domain.GetInt64("id") == domainID {
+ return domain
+ }
+ }
+ return domains[0]
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go
new file mode 100644
index 0000000..f3762e4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go
@@ -0,0 +1,344 @@
+package apps
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type CustomRecordsCreatePopupAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CustomRecordsCreatePopupAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
+ AppId int64
+ DomainId int64
+ RecordId int64
+}) {
+ app := pickApp(params.AppId)
+ this.Data["app"] = app
+ domains := mockDomains(app.GetInt64("id"))
+ domain := pickDomainFromDomains(domains, params.DomainId)
+ this.Data["domain"] = domain
+
+ record := maps.Map{
+ "id": int64(0),
+ "domain": domain.GetString("name"),
+ "lineScope": "china",
+ "lineCarrier": "默认",
+ "lineRegion": "默认",
+ "lineProvince": "默认",
+ "lineContinent": "默认",
+ "lineCountry": "默认",
+ "ruleName": "",
+ "weightEnabled": false,
+ "ttl": 30,
+ "isOn": true,
+ "sdnsParamsJson": "[]",
+ "recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
+ }
+
+ if params.RecordId > 0 {
+ existing := findCustomRecord(app.GetInt64("id"), params.RecordId)
+ if len(existing) > 0 {
+ record["id"] = existing.GetInt64("id")
+ if len(record.GetString("domain")) == 0 {
+ record["domain"] = existing.GetString("domain")
+ }
+
+ record["lineScope"] = strings.TrimSpace(existing.GetString("lineScope"))
+ record["lineCarrier"] = strings.TrimSpace(existing.GetString("lineCarrier"))
+ record["lineRegion"] = strings.TrimSpace(existing.GetString("lineRegion"))
+ record["lineProvince"] = strings.TrimSpace(existing.GetString("lineProvince"))
+ record["lineContinent"] = strings.TrimSpace(existing.GetString("lineContinent"))
+ record["lineCountry"] = strings.TrimSpace(existing.GetString("lineCountry"))
+ record["ruleName"] = existing.GetString("ruleName")
+ record["weightEnabled"] = existing.GetBool("weightEnabled")
+ record["ttl"] = existing.GetInt("ttl")
+ record["isOn"] = existing.GetBool("isOn")
+
+ sdnsParams, _ := existing["sdnsParams"].([]maps.Map)
+ record["sdnsParamsJson"] = marshalJSON(sdnsParams, "[]")
+
+ recordItems := make([]maps.Map, 0)
+ recordType := strings.ToUpper(strings.TrimSpace(existing.GetString("recordType")))
+ values, _ := existing["recordValues"].([]maps.Map)
+ for _, item := range values {
+ itemType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
+ if len(itemType) == 0 {
+ itemType = recordType
+ }
+ if itemType != "A" && itemType != "AAAA" {
+ itemType = "A"
+ }
+
+ recordItems = append(recordItems, maps.Map{
+ "type": itemType,
+ "value": strings.TrimSpace(item.GetString("value")),
+ "weight": item.GetInt("weight"),
+ })
+ }
+ if len(recordItems) == 0 {
+ recordItems = append(recordItems, maps.Map{
+ "type": "A",
+ "value": "",
+ "weight": 100,
+ })
+ }
+ record["recordItemsJson"] = marshalJSON(recordItems, "[]")
+ }
+ }
+
+ if record.GetString("lineScope") != "china" && record.GetString("lineScope") != "overseas" {
+ if len(strings.TrimSpace(record.GetString("lineContinent"))) > 0 || len(strings.TrimSpace(record.GetString("lineCountry"))) > 0 {
+ record["lineScope"] = "overseas"
+ } else {
+ record["lineScope"] = "china"
+ }
+ }
+ if len(record.GetString("lineCarrier")) == 0 {
+ record["lineCarrier"] = "默认"
+ }
+ if len(record.GetString("lineRegion")) == 0 {
+ record["lineRegion"] = "默认"
+ }
+ if len(record.GetString("lineProvince")) == 0 {
+ record["lineProvince"] = "默认"
+ }
+ if len(record.GetString("lineContinent")) == 0 {
+ record["lineContinent"] = "默认"
+ }
+ if len(record.GetString("lineCountry")) == 0 {
+ record["lineCountry"] = "默认"
+ }
+
+ this.Data["record"] = record
+ this.Data["isEditing"] = params.RecordId > 0
+ this.Show()
+}
+
+func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
+ AppId int64
+ DomainId int64
+
+ RecordId int64
+ Domain string
+ LineScope string
+
+ LineCarrier string
+ LineRegion string
+ LineProvince string
+ LineContinent string
+ LineCountry string
+
+ RuleName string
+ SDNSParamsJSON string
+ RecordItemsJSON string
+ WeightEnabled bool
+ TTL int
+ IsOn bool
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "please select app")
+
+ params.Domain = strings.TrimSpace(params.Domain)
+ params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
+ params.RuleName = strings.TrimSpace(params.RuleName)
+ params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
+ params.RecordItemsJSON = strings.TrimSpace(params.RecordItemsJSON)
+
+ domain := maps.Map{}
+ if params.DomainId > 0 {
+ domain = pickDomainFromDomains(mockDomains(params.AppId), params.DomainId)
+ }
+ if len(domain) > 0 {
+ params.Domain = strings.TrimSpace(domain.GetString("name"))
+ }
+ if len(params.Domain) == 0 {
+ this.Fail("please select domain")
+ return
+ }
+
+ if params.LineScope != "china" && params.LineScope != "overseas" {
+ params.LineScope = "china"
+ }
+ if len(params.RuleName) == 0 {
+ this.Fail("please input rule name")
+ return
+ }
+ if params.TTL <= 0 || params.TTL > 86400 {
+ this.Fail("ttl should be in 1-86400")
+ return
+ }
+
+ sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+ if len(sdnsParams) > 10 {
+ this.Fail("sdns params should be <= 10")
+ return
+ }
+
+ recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+ if len(recordValues) == 0 {
+ this.Fail("please input record values")
+ return
+ }
+ if len(recordValues) > 10 {
+ this.Fail("record values should be <= 10")
+ return
+ }
+
+ lineCarrier := strings.TrimSpace(params.LineCarrier)
+ lineRegion := strings.TrimSpace(params.LineRegion)
+ lineProvince := strings.TrimSpace(params.LineProvince)
+ lineContinent := strings.TrimSpace(params.LineContinent)
+ lineCountry := strings.TrimSpace(params.LineCountry)
+ if len(lineCarrier) == 0 {
+ lineCarrier = "默认"
+ }
+ if len(lineRegion) == 0 {
+ lineRegion = "默认"
+ }
+ if len(lineProvince) == 0 {
+ lineProvince = "默认"
+ }
+ if len(lineContinent) == 0 {
+ lineContinent = "默认"
+ }
+ if len(lineCountry) == 0 {
+ lineCountry = "默认"
+ }
+
+ if params.LineScope == "overseas" {
+ lineCarrier = ""
+ lineRegion = ""
+ lineProvince = ""
+ } else {
+ lineContinent = ""
+ lineCountry = ""
+ }
+
+ recordType := recordValues[0].GetString("type")
+ if len(recordType) == 0 {
+ recordType = "A"
+ }
+
+ saveCustomRecord(params.AppId, maps.Map{
+ "id": params.RecordId,
+ "domain": params.Domain,
+ "lineScope": params.LineScope,
+ "lineCarrier": lineCarrier,
+ "lineRegion": lineRegion,
+ "lineProvince": lineProvince,
+ "lineContinent": lineContinent,
+ "lineCountry": lineCountry,
+ "ruleName": params.RuleName,
+ "sdnsParams": sdnsParams,
+ "recordType": recordType,
+ "recordValues": recordValues,
+ "weightEnabled": params.WeightEnabled,
+ "ttl": params.TTL,
+ "isOn": params.IsOn,
+ })
+
+ this.Success()
+}
+
+func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
+ if len(raw) == 0 {
+ return []maps.Map{}, nil
+ }
+
+ list := []maps.Map{}
+ if err := json.Unmarshal([]byte(raw), &list); err != nil {
+ return nil, fmt.Errorf("sdns params json is invalid")
+ }
+
+ result := make([]maps.Map, 0, len(list))
+ for _, item := range list {
+ name := strings.TrimSpace(item.GetString("name"))
+ value := strings.TrimSpace(item.GetString("value"))
+ if len(name) == 0 && len(value) == 0 {
+ continue
+ }
+ if len(name) < 2 || len(name) > 64 {
+ return nil, fmt.Errorf("sdns param name length should be in 2-64")
+ }
+ if len(value) < 1 || len(value) > 64 {
+ return nil, fmt.Errorf("sdns param value length should be in 1-64")
+ }
+ result = append(result, maps.Map{
+ "name": name,
+ "value": value,
+ })
+ }
+
+ return result, nil
+}
+
+func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
+ if len(raw) == 0 {
+ return []maps.Map{}, nil
+ }
+
+ list := []maps.Map{}
+ if err := json.Unmarshal([]byte(raw), &list); err != nil {
+ return nil, fmt.Errorf("record items json is invalid")
+ }
+
+ result := make([]maps.Map, 0, len(list))
+ for _, item := range list {
+ recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
+ recordValue := strings.TrimSpace(item.GetString("value"))
+ if len(recordType) == 0 && len(recordValue) == 0 {
+ continue
+ }
+ if recordType != "A" && recordType != "AAAA" {
+ return nil, fmt.Errorf("record type should be A or AAAA")
+ }
+ if len(recordValue) == 0 {
+ return nil, fmt.Errorf("record value should not be empty")
+ }
+
+ weight := item.GetInt("weight")
+ if !weightEnabled {
+ weight = 100
+ }
+ if weight < 1 || weight > 100 {
+ return nil, fmt.Errorf("weight should be in 1-100")
+ }
+
+ result = append(result, maps.Map{
+ "type": recordType,
+ "value": recordValue,
+ "weight": weight,
+ })
+ }
+
+ return result, nil
+}
+
+func marshalJSON(v interface{}, fallback string) string {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return fallback
+ }
+ return string(b)
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go
new file mode 100644
index 0000000..4d8777c
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go
@@ -0,0 +1,17 @@
+package apps
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type CustomRecordsDeleteAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CustomRecordsDeleteAction) RunPost(params struct {
+ AppId int64
+ RecordId int64
+}) {
+ if params.AppId > 0 && params.RecordId > 0 {
+ deleteCustomRecord(params.AppId, params.RecordId)
+ }
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go
new file mode 100644
index 0000000..0e16fd3
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go
@@ -0,0 +1,18 @@
+package apps
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type CustomRecordsToggleAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CustomRecordsToggleAction) RunPost(params struct {
+ AppId int64
+ RecordId int64
+ IsOn bool
+}) {
+ if params.AppId > 0 && params.RecordId > 0 {
+ toggleCustomRecord(params.AppId, params.RecordId, params.IsOn)
+ }
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go
new file mode 100644
index 0000000..a29f554
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go
@@ -0,0 +1,264 @@
+package apps
+
+import (
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+var customRecordStore = struct {
+ sync.RWMutex
+ nextID int64
+ data map[int64][]maps.Map
+}{
+ nextID: 1000,
+ data: map[int64][]maps.Map{
+ 1: {
+ {
+ "id": int64(1001),
+ "domain": "api.business.com",
+ "lineScope": "china",
+ "lineCarrier": "电信",
+ "lineRegion": "华东",
+ "lineProvince": "上海",
+ "ruleName": "上海电信灰度-v2",
+ "sdnsParams": []maps.Map{
+ {
+ "name": "app_ver",
+ "value": "2.3.1",
+ },
+ },
+ "recordType": "A",
+ "recordValues": []maps.Map{{"type": "A", "value": "1.1.1.10", "weight": 100}},
+ "weightEnabled": false,
+ "ttl": 30,
+ "isOn": true,
+ "updatedAt": "2026-02-23 10:20:00",
+ },
+ },
+ },
+}
+
+func loadCustomRecords(appID int64) []maps.Map {
+ customRecordStore.RLock()
+ defer customRecordStore.RUnlock()
+
+ records := customRecordStore.data[appID]
+ result := make([]maps.Map, 0, len(records))
+ for _, record := range records {
+ result = append(result, cloneCustomRecord(record))
+ }
+ return result
+}
+
+func countCustomRecordsByDomain(appID int64, domain string) int {
+ domain = strings.ToLower(strings.TrimSpace(domain))
+ if len(domain) == 0 {
+ return 0
+ }
+
+ customRecordStore.RLock()
+ defer customRecordStore.RUnlock()
+
+ count := 0
+ for _, record := range customRecordStore.data[appID] {
+ if strings.ToLower(strings.TrimSpace(record.GetString("domain"))) == domain {
+ count++
+ }
+ }
+ return count
+}
+
+func findCustomRecord(appID int64, recordID int64) maps.Map {
+ for _, record := range loadCustomRecords(appID) {
+ if record.GetInt64("id") == recordID {
+ return record
+ }
+ }
+ return maps.Map{}
+}
+
+func saveCustomRecord(appID int64, record maps.Map) maps.Map {
+ customRecordStore.Lock()
+ defer customRecordStore.Unlock()
+
+ if appID <= 0 {
+ return maps.Map{}
+ }
+
+ record = cloneCustomRecord(record)
+ recordID := record.GetInt64("id")
+ if recordID <= 0 {
+ customRecordStore.nextID++
+ recordID = customRecordStore.nextID
+ record["id"] = recordID
+ }
+ record["updatedAt"] = nowCustomRecordTime()
+
+ records := customRecordStore.data[appID]
+ found := false
+ for i, oldRecord := range records {
+ if oldRecord.GetInt64("id") == recordID {
+ records[i] = cloneCustomRecord(record)
+ found = true
+ break
+ }
+ }
+ if !found {
+ records = append(records, cloneCustomRecord(record))
+ }
+ customRecordStore.data[appID] = records
+
+ return cloneCustomRecord(record)
+}
+
+func deleteCustomRecord(appID int64, recordID int64) {
+ customRecordStore.Lock()
+ defer customRecordStore.Unlock()
+
+ records := customRecordStore.data[appID]
+ if len(records) == 0 {
+ return
+ }
+
+ filtered := make([]maps.Map, 0, len(records))
+ for _, record := range records {
+ if record.GetInt64("id") == recordID {
+ continue
+ }
+ filtered = append(filtered, record)
+ }
+ customRecordStore.data[appID] = filtered
+}
+
+func toggleCustomRecord(appID int64, recordID int64, isOn bool) {
+ customRecordStore.Lock()
+ defer customRecordStore.Unlock()
+
+ records := customRecordStore.data[appID]
+ for i, record := range records {
+ if record.GetInt64("id") == recordID {
+ record["isOn"] = isOn
+ record["updatedAt"] = nowCustomRecordTime()
+ records[i] = record
+ break
+ }
+ }
+ customRecordStore.data[appID] = records
+}
+
+func cloneCustomRecord(src maps.Map) maps.Map {
+ dst := maps.Map{}
+ for k, v := range src {
+ switch k {
+ case "sdnsParams", "recordValues":
+ if list, ok := v.([]maps.Map); ok {
+ cloned := make([]maps.Map, 0, len(list))
+ for _, item := range list {
+ m := maps.Map{}
+ for k2, v2 := range item {
+ m[k2] = v2
+ }
+ cloned = append(cloned, m)
+ }
+ dst[k] = cloned
+ } else {
+ dst[k] = []maps.Map{}
+ }
+ default:
+ dst[k] = v
+ }
+ }
+ return dst
+}
+
+func nowCustomRecordTime() string {
+ return time.Now().Format("2006-01-02 15:04:05")
+}
+
+func buildLineText(record maps.Map) string {
+ parts := []string{}
+ if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
+ parts = append(parts,
+ strings.TrimSpace(record.GetString("lineContinent")),
+ strings.TrimSpace(record.GetString("lineCountry")),
+ )
+ } else {
+ parts = append(parts,
+ strings.TrimSpace(record.GetString("lineCarrier")),
+ strings.TrimSpace(record.GetString("lineRegion")),
+ strings.TrimSpace(record.GetString("lineProvince")),
+ )
+ }
+
+ finalParts := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if len(part) == 0 || part == "默认" {
+ continue
+ }
+ finalParts = append(finalParts, part)
+ }
+ if len(finalParts) == 0 {
+ return "默认"
+ }
+ return strings.Join(finalParts, " / ")
+}
+
+func buildParamsText(record maps.Map) string {
+ params, ok := record["sdnsParams"].([]maps.Map)
+ if !ok || len(params) == 0 {
+ return "-"
+ }
+
+ parts := make([]string, 0, len(params))
+ for _, param := range params {
+ name := strings.TrimSpace(param.GetString("name"))
+ value := strings.TrimSpace(param.GetString("value"))
+ if len(name) == 0 || len(value) == 0 {
+ continue
+ }
+ parts = append(parts, name+"="+value)
+ }
+ if len(parts) == 0 {
+ return "-"
+ }
+ return strings.Join(parts, "; ")
+}
+
+func buildRecordValueText(record maps.Map) string {
+ values, ok := record["recordValues"].([]maps.Map)
+ if !ok || len(values) == 0 {
+ return "-"
+ }
+
+ weightEnabled := record.GetBool("weightEnabled")
+ defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
+ parts := make([]string, 0, len(values))
+ for _, item := range values {
+ value := strings.TrimSpace(item.GetString("value"))
+ if len(value) == 0 {
+ continue
+ }
+ recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
+ if len(recordType) == 0 {
+ recordType = defaultType
+ }
+ if recordType != "A" && recordType != "AAAA" {
+ recordType = "A"
+ }
+ part := recordType + " " + value
+ if weightEnabled {
+ part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
+ } else {
+ // no extra suffix
+ }
+ parts = append(parts, part)
+ }
+ if len(parts) == 0 {
+ return "-"
+ }
+ return strings.Join(parts, ", ")
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go
new file mode 100644
index 0000000..2833df8
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go
@@ -0,0 +1,31 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type DomainsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *DomainsAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *DomainsAction) RunGet(params struct {
+ AppId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ app := pickApp(params.AppId)
+
+ domains := mockDomains(app.GetInt64("id"))
+ for _, domain := range domains {
+ domainName := domain.GetString("name")
+ domain["customRecordCount"] = countCustomRecordsByDomain(app.GetInt64("id"), domainName)
+ }
+
+ this.Data["app"] = app
+ this.Data["domains"] = domains
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go
new file mode 100644
index 0000000..ef26367
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go
@@ -0,0 +1,32 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type DomainsCreatePopupAction struct {
+ actionutils.ParentAction
+}
+
+func (this *DomainsCreatePopupAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *DomainsCreatePopupAction) RunGet(params struct {
+ AppId int64
+}) {
+ this.Data["app"] = pickApp(params.AppId)
+ this.Show()
+}
+
+func (this *DomainsCreatePopupAction) RunPost(params struct {
+ AppId int64
+ Domain string
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("domain", params.Domain).Require("please input domain")
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go
new file mode 100644
index 0000000..8bd72d4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go
@@ -0,0 +1,14 @@
+package apps
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type DomainsDeleteAction struct {
+ actionutils.ParentAction
+}
+
+func (this *DomainsDeleteAction) RunPost(params struct {
+ DomainId int64
+}) {
+ _ = params.DomainId
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
new file mode 100644
index 0000000..bb17da6
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
@@ -0,0 +1,23 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *IndexAction) RunGet(params struct {
+ Keyword string
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["keyword"] = params.Keyword
+ this.Data["apps"] = filterApps(params.Keyword, "", "", "")
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
new file mode 100644
index 0000000..28bcb4c
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
@@ -0,0 +1,32 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "app").
+ Prefix("/httpdns/apps").
+ Get("", new(IndexAction)).
+ Get("/app", new(AppAction)).
+ GetPost("/app/settings", new(AppSettingsAction)).
+ Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
+ Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
+ Post("/app/settings/resetAESSecret", new(AppSettingsResetAESSecretAction)).
+ Get("/domains", new(DomainsAction)).
+ Get("/customRecords", new(CustomRecordsAction)).
+ GetPost("/createPopup", new(CreatePopupAction)).
+ GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
+ Post("/domains/delete", new(DomainsDeleteAction)).
+ GetPost("/customRecords/createPopup", new(CustomRecordsCreatePopupAction)).
+ Post("/customRecords/delete", new(CustomRecordsDeleteAction)).
+ Post("/customRecords/toggle", new(CustomRecordsToggleAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
new file mode 100644
index 0000000..fe383c5
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
@@ -0,0 +1,128 @@
+package apps
+
+import (
+ "strings"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+func mockApps() []maps.Map {
+ return []maps.Map{
+ {
+ "id": int64(1),
+ "name": "主站移动业务",
+ "appId": "ab12xc34s2",
+ "clusterId": int64(1),
+ "domainCount": 3,
+ "isOn": true,
+ "authStatus": "enabled",
+ "ecsMode": "auto",
+ "pinningMode": "report",
+ "sanMode": "strict",
+ "riskLevel": "medium",
+ "riskSummary": "Pinning 处于观察模式",
+ "secretVersion": "v2026.02.20",
+ },
+ {
+ "id": int64(2),
+ "name": "视频网关业务",
+ "appId": "vd8992ksm1",
+ "clusterId": int64(2),
+ "domainCount": 1,
+ "isOn": true,
+ "authStatus": "enabled",
+ "ecsMode": "custom",
+ "pinningMode": "enforce",
+ "sanMode": "strict",
+ "riskLevel": "low",
+ "riskSummary": "已启用强校验",
+ "secretVersion": "v2026.02.18",
+ },
+ {
+ "id": int64(3),
+ "name": "海外灰度测试",
+ "appId": "ov7711hkq9",
+ "clusterId": int64(1),
+ "domainCount": 2,
+ "isOn": false,
+ "authStatus": "disabled",
+ "ecsMode": "off",
+ "pinningMode": "off",
+ "sanMode": "report",
+ "riskLevel": "high",
+ "riskSummary": "应用关闭且证书策略偏弱",
+ "secretVersion": "v2026.01.30",
+ },
+ }
+}
+
+func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode string) []maps.Map {
+ all := mockApps()
+ if len(keyword) == 0 && len(riskLevel) == 0 && len(ecsMode) == 0 && len(pinningMode) == 0 {
+ return all
+ }
+
+ keyword = strings.ToLower(strings.TrimSpace(keyword))
+ result := make([]maps.Map, 0)
+ for _, app := range all {
+ if len(keyword) > 0 {
+ name := strings.ToLower(app.GetString("name"))
+ appID := strings.ToLower(app.GetString("appId"))
+ if !strings.Contains(name, keyword) && !strings.Contains(appID, keyword) {
+ continue
+ }
+ }
+ if len(riskLevel) > 0 && app.GetString("riskLevel") != riskLevel {
+ continue
+ }
+ if len(ecsMode) > 0 && app.GetString("ecsMode") != ecsMode {
+ continue
+ }
+ if len(pinningMode) > 0 && app.GetString("pinningMode") != pinningMode {
+ continue
+ }
+ result = append(result, app)
+ }
+
+ return result
+}
+
+func pickApp(appID int64) maps.Map {
+ apps := mockApps()
+ if appID <= 0 {
+ return apps[0]
+ }
+ for _, app := range apps {
+ if app.GetInt64("id") == appID {
+ return app
+ }
+ }
+ return apps[0]
+}
+
+func mockDomains(appID int64) []maps.Map {
+ _ = appID
+ return []maps.Map{
+ {
+ "id": int64(101),
+ "name": "api.business.com",
+ },
+ {
+ "id": int64(102),
+ "name": "payment.business.com",
+ },
+ }
+}
+
+func pickDomain(domainID int64) maps.Map {
+ domains := mockDomains(0)
+ if domainID <= 0 {
+ return domains[0]
+ }
+ for _, domain := range domains {
+ if domain.GetInt64("id") == domainID {
+ return domain
+ }
+ }
+ return domains[0]
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go
new file mode 100644
index 0000000..36c5296
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go
@@ -0,0 +1,85 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type PoliciesAction struct {
+ actionutils.ParentAction
+}
+
+func (this *PoliciesAction) Init() {
+ this.Nav("httpdns", "app", "")
+}
+
+func (this *PoliciesAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["policies"] = loadGlobalPolicies()
+ this.Show()
+}
+
+func (this *PoliciesAction) RunPost(params struct {
+ DefaultTTL int
+ DefaultSniPolicy string
+ DefaultFallbackMs int
+ ECSMode string
+ ECSIPv4Prefix int
+ ECSIPv6Prefix int
+ PinningMode string
+ SANMode string
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "默认 TTL 需要大于 0")
+ params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "默认超时需要大于 0")
+
+ if params.DefaultTTL > 86400 {
+ this.Fail("默认 TTL 不能超过 86400 秒")
+ return
+ }
+ if params.DefaultFallbackMs > 10000 {
+ this.Fail("默认超时不能超过 10000 毫秒")
+ return
+ }
+ if params.DefaultSniPolicy != "level1" && params.DefaultSniPolicy != "level2" && params.DefaultSniPolicy != "level3" {
+ this.Fail("默认 SNI 等级不正确")
+ return
+ }
+ if params.ECSMode != "off" && params.ECSMode != "auto" && params.ECSMode != "custom" {
+ this.Fail("ECS 模式不正确")
+ return
+ }
+ if params.ECSIPv4Prefix < 0 || params.ECSIPv4Prefix > 32 {
+ this.Fail("IPv4 掩码范围是 0-32")
+ return
+ }
+ if params.ECSIPv6Prefix < 0 || params.ECSIPv6Prefix > 128 {
+ this.Fail("IPv6 掩码范围是 0-128")
+ return
+ }
+ if params.PinningMode != "off" && params.PinningMode != "report" && params.PinningMode != "enforce" {
+ this.Fail("Pinning 策略不正确")
+ return
+ }
+ if params.SANMode != "off" && params.SANMode != "report" && params.SANMode != "strict" {
+ this.Fail("SAN 策略不正确")
+ return
+ }
+
+ saveGlobalPolicies(maps.Map{
+ "defaultTTL": params.DefaultTTL,
+ "defaultSniPolicy": params.DefaultSniPolicy,
+ "defaultFallbackMs": params.DefaultFallbackMs,
+ "ecsMode": params.ECSMode,
+ "ecsIPv4Prefix": params.ECSIPv4Prefix,
+ "ecsIPv6Prefix": params.ECSIPv6Prefix,
+ "pinningMode": params.PinningMode,
+ "sanMode": params.SANMode,
+ })
+
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go
new file mode 100644
index 0000000..08aee8f
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go
@@ -0,0 +1,54 @@
+package apps
+
+import (
+ "sync"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+var globalPoliciesStore = struct {
+ sync.RWMutex
+ data maps.Map
+}{
+ data: maps.Map{
+ "defaultTTL": 30,
+ "defaultSniPolicy": "level2",
+ "defaultFallbackMs": 300,
+ "ecsMode": "auto",
+ "ecsIPv4Prefix": 24,
+ "ecsIPv6Prefix": 56,
+ "pinningMode": "report",
+ "sanMode": "strict",
+ },
+}
+
+func loadGlobalPolicies() maps.Map {
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+
+ return maps.Map{
+ "defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
+ "defaultSniPolicy": globalPoliciesStore.data.GetString("defaultSniPolicy"),
+ "defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
+ "ecsMode": globalPoliciesStore.data.GetString("ecsMode"),
+ "ecsIPv4Prefix": globalPoliciesStore.data.GetInt("ecsIPv4Prefix"),
+ "ecsIPv6Prefix": globalPoliciesStore.data.GetInt("ecsIPv6Prefix"),
+ "pinningMode": globalPoliciesStore.data.GetString("pinningMode"),
+ "sanMode": globalPoliciesStore.data.GetString("sanMode"),
+ }
+}
+
+func saveGlobalPolicies(policies maps.Map) {
+ globalPoliciesStore.Lock()
+ globalPoliciesStore.data = maps.Map{
+ "defaultTTL": policies.GetInt("defaultTTL"),
+ "defaultSniPolicy": policies.GetString("defaultSniPolicy"),
+ "defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
+ "ecsMode": policies.GetString("ecsMode"),
+ "ecsIPv4Prefix": policies.GetInt("ecsIPv4Prefix"),
+ "ecsIPv6Prefix": policies.GetInt("ecsIPv6Prefix"),
+ "pinningMode": policies.GetString("pinningMode"),
+ "sanMode": policies.GetString("sanMode"),
+ }
+ globalPoliciesStore.Unlock()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go
new file mode 100644
index 0000000..645d21a
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go
@@ -0,0 +1,21 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+)
+
+type CertsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CertsAction) Init() {
+ this.Nav("httpdns", "cluster", "certs")
+}
+
+func (this *CertsAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["certs"] = policies.LoadPublicSNICertificates()
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go
new file mode 100644
index 0000000..63edfa2
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go
@@ -0,0 +1,10 @@
+package clusters
+import (
+"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+"github.com/iwind/TeaGo/maps"
+)
+type CheckPortsAction struct { actionutils.ParentAction }
+func (this *CheckPortsAction) RunPost(params struct{ NodeId int64 }) {
+this.Data["results"] = []maps.Map{}
+this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go
new file mode 100644
index 0000000..de5cf5d
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go
@@ -0,0 +1,34 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type ClusterAction struct {
+ actionutils.ParentAction
+}
+
+func (this *ClusterAction) Init() {
+ this.Nav("httpdns", "cluster", "index")
+}
+
+func (this *ClusterAction) RunGet(params struct {
+ ClusterId int64
+ InstalledState int
+ ActiveState int
+ Keyword string
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ cluster := pickCluster(params.ClusterId)
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["cluster"] = cluster
+ this.Data["installState"] = params.InstalledState
+ this.Data["activeState"] = params.ActiveState
+ this.Data["keyword"] = params.Keyword
+ nodes := mockNodes(params.ClusterId, params.InstalledState, params.ActiveState, params.Keyword)
+ this.Data["nodes"] = nodes
+ this.Data["hasNodes"] = len(nodes) > 0
+ this.Data["page"] = ""
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go
new file mode 100644
index 0000000..c516d9e
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go
@@ -0,0 +1,68 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("", "node", "node")
+ this.SecondMenu("nodes")
+}
+
+func (this *IndexAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+}) {
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+
+ this.Data["nodeDatetime"] = "2026-02-22 12:00:00"
+ this.Data["nodeTimeDiff"] = 0
+ this.Data["shouldUpgrade"] = false
+ this.Data["newVersion"] = ""
+
+ this.Data["node"] = maps.Map{
+ "id": params.NodeId,
+ "name": "Mock HTTPDNS Node",
+ "ipAddresses": []maps.Map{{"ip": "100.200.100.200", "name": "Public IP", "canAccess": true, "isOn": true, "isUp": true}},
+ "cluster": maps.Map{"id": params.ClusterId, "name": "Mock Cluster", "installDir": "/opt/edge-httpdns"},
+ "installDir": "/opt/edge-httpdns",
+ "isInstalled": true,
+ "uniqueId": "m-1234567890",
+ "secret": "mock-secret-key",
+ "isOn": true,
+ "isUp": true,
+ "apiNodeAddrs": []string{"192.168.1.100:8001"},
+ "login": nil,
+
+ "status": maps.Map{
+ "isActive": true,
+ "updatedAt": 1670000000,
+ "hostname": "node-01.local",
+ "cpuUsage": 0.15,
+ "cpuUsageText": "15.00%",
+ "memUsage": 0.45,
+ "memUsageText": "45.00%",
+ "connectionCount": 100,
+ "buildVersion": "1.0.0",
+ "cpuPhysicalCount": 4,
+ "cpuLogicalCount": 8,
+ "load1m": "0.50",
+ "load5m": "0.60",
+ "load15m": "0.70",
+ "cacheTotalDiskSize": "10G",
+ "cacheTotalMemorySize": "2G",
+ "exePath": "/opt/edge-httpdns/bin/edge-httpdns",
+ "apiSuccessPercent": 100.0,
+ "apiAvgCostSeconds": 0.05,
+ },
+ }
+
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go
new file mode 100644
index 0000000..eeb79a6
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go
@@ -0,0 +1,41 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type InstallAction struct {
+ actionutils.ParentAction
+}
+
+func (this *InstallAction) Init() {
+ this.Nav("", "node", "install")
+ this.SecondMenu("nodes")
+}
+
+func (this *InstallAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+
+ this.Data["apiEndpoints"] = "\"http://127.0.0.1:7788\""
+ this.Data["sshAddr"] = "192.168.1.100:22"
+
+ this.Data["node"] = maps.Map{
+ "id": params.NodeId,
+ "name": "Mock Node",
+ "isInstalled": false,
+ "uniqueId": "m-1234567890",
+ "secret": "mock-secret-key",
+ "installDir": "/opt/edge-httpdns",
+ "cluster": maps.Map{"installDir": "/opt/edge-httpdns"},
+ }
+ this.Data["installStatus"] = nil
+
+ this.Show()
+}
+
+func (this *InstallAction) RunPost(params struct{ NodeId int64 }) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go
new file mode 100644
index 0000000..e88e779
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go
@@ -0,0 +1,31 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type LogsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *LogsAction) Init() {
+ this.Nav("", "node", "log")
+ this.SecondMenu("nodes")
+}
+
+func (this *LogsAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+
+ this.Data["dayFrom"] = ""
+ this.Data["dayTo"] = ""
+ this.Data["keyword"] = ""
+ this.Data["level"] = ""
+ this.Data["logs"] = []maps.Map{}
+ this.Data["page"] = ""
+ this.Data["node"] = maps.Map{"id": params.NodeId, "name": "Mock Node"}
+
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go
new file mode 100644
index 0000000..de506c5
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go
@@ -0,0 +1,13 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+)
+
+type StartAction struct {
+ actionutils.ParentAction
+}
+
+func (this *StartAction) RunPost(params struct{ NodeId int64 }) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go
new file mode 100644
index 0000000..a85d567
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go
@@ -0,0 +1,27 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type StatusAction struct {
+ actionutils.ParentAction
+}
+
+func (this *StatusAction) Init() {
+ this.Nav("", "node", "status")
+ this.SecondMenu("nodes")
+}
+
+func (this *StatusAction) RunPost(params struct{ NodeId int64 }) {
+ this.Data["installStatus"] = maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ "errorCode": "",
+ }
+ this.Data["isInstalled"] = true
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go
new file mode 100644
index 0000000..10b3ad3
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go
@@ -0,0 +1,13 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+)
+
+type StopAction struct {
+ actionutils.ParentAction
+}
+
+func (this *StopAction) RunPost(params struct{ NodeId int64 }) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go
new file mode 100644
index 0000000..1e86260
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go
@@ -0,0 +1,41 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type UpdateAction struct {
+ actionutils.ParentAction
+}
+
+func (this *UpdateAction) Init() {
+ this.Nav("", "node", "update")
+ this.SecondMenu("nodes")
+}
+
+func (this *UpdateAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+
+ this.Data["clusters"] = []maps.Map{{"id": params.ClusterId, "name": "Mock Cluster"}}
+ this.Data["loginId"] = 0
+ this.Data["sshHost"] = "192.168.1.100"
+ this.Data["sshPort"] = 22
+ this.Data["grant"] = nil
+ this.Data["apiNodeAddrs"] = []string{}
+
+ this.Data["node"] = maps.Map{
+ "id": params.NodeId,
+ "name": "Mock Node",
+ "isOn": true,
+ "ipAddresses": []maps.Map{},
+ }
+
+ this.Show()
+}
+
+func (this *UpdateAction) RunPost(params struct{ NodeId int64 }) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go
new file mode 100644
index 0000000..f655c57
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go
@@ -0,0 +1,13 @@
+package node
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+)
+
+type UpdateInstallStatusAction struct {
+ actionutils.ParentAction
+}
+
+func (this *UpdateInstallStatusAction) RunPost(params struct{ NodeId int64 }) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go
new file mode 100644
index 0000000..f025b0f
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go
@@ -0,0 +1,49 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type ClusterSettingsAction struct {
+ actionutils.ParentAction
+}
+
+func (this *ClusterSettingsAction) Init() {
+ this.Nav("httpdns", "cluster", "settings")
+}
+
+func (this *ClusterSettingsAction) RunGet(params struct {
+ ClusterId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ cluster := pickCluster(params.ClusterId)
+ installDir := cluster.GetString("installDir")
+ if len(installDir) == 0 {
+ installDir = "/opt/edge-httpdns"
+ }
+ this.Data["cluster"] = cluster
+ this.Data["settings"] = map[string]interface{}{
+ "region": cluster.GetString("region"),
+ "gatewayDomain": cluster.GetString("gatewayDomain"),
+ "cacheTtl": cluster.GetInt("cacheTtl"),
+ "fallbackTimeout": cluster.GetInt("fallbackTimeout"),
+ "installDir": installDir,
+ "isOn": cluster.GetBool("isOn"),
+ }
+ this.Show()
+}
+
+func (this *ClusterSettingsAction) RunPost(params struct {
+ ClusterId int64
+ Name string
+ Region string
+ GatewayDomain string
+ CacheTtl int32
+ FallbackTimeout int32
+ InstallDir string
+ IsOn bool
+}) {
+ // Mock successful save
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go
new file mode 100644
index 0000000..e8779f5
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go
@@ -0,0 +1,19 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type CreateAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CreateAction) Init() {
+ this.Nav("httpdns", "cluster", "")
+}
+
+func (this *CreateAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go
new file mode 100644
index 0000000..65dc880
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go
@@ -0,0 +1,30 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type CreateNodeAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CreateNodeAction) Init() {
+ this.Nav("", "node", "createNode")
+ this.SecondMenu("nodes")
+}
+
+func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["cluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+ this.Show()
+}
+
+func (this *CreateNodeAction) RunPost(params struct {
+ ClusterId int64
+ Name string
+}) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go
new file mode 100644
index 0000000..57c246b
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go
@@ -0,0 +1,30 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type DeleteAction struct {
+ actionutils.ParentAction
+}
+
+func (this *DeleteAction) Init() {
+ this.Nav("httpdns", "cluster", "delete")
+}
+
+func (this *DeleteAction) RunGet(params struct {
+ ClusterId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ cluster := pickCluster(params.ClusterId)
+ this.Data["cluster"] = cluster
+ this.Show()
+}
+
+func (this *DeleteAction) RunPost(params struct {
+ ClusterId int64
+}) {
+ _ = params.ClusterId
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go
new file mode 100644
index 0000000..7adcd40
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go
@@ -0,0 +1,6 @@
+package clusters
+import (
+"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+)
+type DeleteNodeAction struct { actionutils.ParentAction }
+func (this *DeleteNodeAction) RunPost(params struct{ ClusterId int64; NodeId int64 }) { this.Success() }
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go
new file mode 100644
index 0000000..f1ead83
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go
@@ -0,0 +1,20 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "cluster", "")
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["clusters"] = mockClusters()
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go
new file mode 100644
index 0000000..bfecd53
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go
@@ -0,0 +1,41 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "cluster").
+ Prefix("/httpdns/clusters").
+ Get("", new(IndexAction)).
+ Get("/create", new(CreateAction)).
+ Get("/cluster", new(ClusterAction)).
+ GetPost("/cluster/settings", new(ClusterSettingsAction)).
+ GetPost("/delete", new(DeleteAction)).
+ // Node level
+ GetPost("/createNode", new(CreateNodeAction)).
+ Post("/deleteNode", new(DeleteNodeAction)).
+ Get("/upgradeRemote", new(UpgradeRemoteAction)).
+ GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
+ Post("/checkPorts", new(CheckPortsAction)).
+
+ // Node internal pages
+ Prefix("/httpdns/clusters/cluster/node").
+ Get("", new(node.IndexAction)).
+ Get("/logs", new(node.LogsAction)).
+ GetPost("/update", new(node.UpdateAction)).
+ GetPost("/install", new(node.InstallAction)).
+ Post("/status", new(node.StatusAction)).
+ Post("/updateInstallStatus", new(node.UpdateInstallStatusAction)).
+ Post("/start", new(node.StartAction)).
+ Post("/stop", new(node.StopAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
new file mode 100644
index 0000000..092eb23
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
@@ -0,0 +1,126 @@
+package clusters
+
+import "github.com/iwind/TeaGo/maps"
+
+func mockClusters() []maps.Map {
+ return []maps.Map{
+ {
+ "id": int64(1),
+ "name": "gateway-cn-hz",
+ "region": "cn-hangzhou",
+ "gatewayDomain": "gw-hz.httpdns.example.com",
+ "installDir": "/opt/edge-httpdns",
+ "countAllNodes": 3,
+ "countActiveNodes": 3,
+ "countApps": 5,
+ "cacheTtl": 30,
+ "fallbackTimeout": 300,
+ "isOn": true,
+ },
+ {
+ "id": int64(2),
+ "name": "gateway-cn-bj",
+ "region": "cn-beijing",
+ "gatewayDomain": "gw-bj.httpdns.example.com",
+ "installDir": "/opt/edge-httpdns",
+ "countAllNodes": 3,
+ "countActiveNodes": 2,
+ "countApps": 2,
+ "cacheTtl": 30,
+ "fallbackTimeout": 300,
+ "isOn": true,
+ },
+ }
+}
+
+func pickCluster(clusterId int64) maps.Map {
+ clusters := mockClusters()
+ if clusterId <= 0 {
+ return clusters[0]
+ }
+ for _, c := range clusters {
+ if c.GetInt64("id") == clusterId {
+ return c
+ }
+ }
+ return clusters[0]
+}
+
+func mockNodes(clusterId int64, installState int, activeState int, keyword string) []maps.Map {
+ _ = clusterId
+ return []maps.Map{
+ {
+ "id": int64(101),
+ "name": "45.250.184.56",
+ "isInstalled": true,
+ "isOn": true,
+ "isUp": true,
+ "installStatus": maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ },
+ "status": maps.Map{
+ "isActive": true,
+ "updatedAt": 1700000000,
+ "hostname": "node-01",
+ "cpuUsage": 0.0253,
+ "cpuUsageText": "2.53%",
+ "memUsage": 0.5972,
+ "memUsageText": "59.72%",
+ "load1m": 0.02,
+ },
+ "ipAddresses": []maps.Map{
+ {
+ "id": 1,
+ "name": "",
+ "ip": "45.250.184.56",
+ "canAccess": true,
+ },
+ },
+ },
+ {
+ "id": int64(102),
+ "name": "45.250.184.53",
+ "isInstalled": true,
+ "isOn": true,
+ "isUp": true,
+ "installStatus": maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ },
+ "status": maps.Map{
+ "isActive": true,
+ "updatedAt": 1700000000,
+ "hostname": "node-02",
+ "cpuUsage": 0.0039,
+ "cpuUsageText": "0.39%",
+ "memUsage": 0.0355,
+ "memUsageText": "3.55%",
+ "load1m": 0.0,
+ },
+ "ipAddresses": []maps.Map{
+ {
+ "id": 2,
+ "name": "",
+ "ip": "45.250.184.53",
+ "canAccess": true,
+ },
+ },
+ },
+ }
+}
+
+func mockCerts() []maps.Map {
+ return []maps.Map{
+ {
+ "id": int64(11),
+ "domain": "resolve.edge.example.com",
+ "issuer": "Mock CA",
+ "expiresAt": "2026-12-31 23:59:59",
+ },
+ }
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go
new file mode 100644
index 0000000..f8360e4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go
@@ -0,0 +1,39 @@
+package clusters
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type UpdateNodeSSHAction struct {
+ actionutils.ParentAction
+}
+
+func (this *UpdateNodeSSHAction) RunGet(params struct {
+ NodeId int64
+}) {
+ this.Data["nodeId"] = params.NodeId
+ this.Data["clusterId"] = 0
+ this.Data["node"] = maps.Map{
+ "id": params.NodeId,
+ "name": "Mock Node",
+ }
+ this.Data["loginId"] = 0
+ this.Data["params"] = maps.Map{
+ "host": "1.2.3.4",
+ "port": 22,
+ "grantId": 0,
+ }
+ this.Data["grant"] = nil
+ this.Show()
+}
+
+func (this *UpdateNodeSSHAction) RunPost(params struct {
+ NodeId int64
+ LoginId int64
+ SshHost string
+ SshPort int
+ GrantId int64
+}) {
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go
new file mode 100644
index 0000000..ba99390
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go
@@ -0,0 +1,16 @@
+package clusters
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type UpgradeRemoteAction struct {
+ actionutils.ParentAction
+}
+
+func (this *UpgradeRemoteAction) RunGet(params struct {
+ NodeId int64
+ ClusterId int64
+}) {
+ this.Data["nodeId"] = params.NodeId
+ this.Data["clusterId"] = params.ClusterId
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
new file mode 100644
index 0000000..e8f7d74
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
@@ -0,0 +1,28 @@
+package ech
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type AuditAction struct {
+ actionutils.ParentAction
+}
+
+func (this *AuditAction) Init() {
+ this.Nav("httpdns", "ech", "")
+}
+
+func (this *AuditAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["auditLogs"] = []map[string]interface{}{
+ {
+ "id": 1,
+ "scope": "tenant:1001 -> domain:api.business.com -> region:cn-hz",
+ "operator": "admin",
+ "result": "success",
+ "createdAt": "2026-02-21 10:00:00",
+ },
+ }
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
new file mode 100644
index 0000000..b33aa5f
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
@@ -0,0 +1,47 @@
+package ech
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "ech", "")
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["health"] = map[string]interface{}{
+ "keySyncRate": 0.9995,
+ "decryptFailRate": 0.0001,
+ "isHealthy": true,
+ }
+
+ this.Data["echLogs"] = []map[string]interface{}{
+ {
+ "id": 1024,
+ "version": "ech-v20260221-01",
+ "recordType": "HTTPS(Type65)",
+ "publicKey": "BFCf8h5Qmock_public_key_for_ui_preview_lx9v2k7p0n",
+ "publishTime": "2026-02-21 00:00:00",
+ "syncStatus": "success",
+ "nodesPending": 0,
+ "isCurrent": true,
+ },
+ {
+ "id": 1023,
+ "version": "ech-v20260220-01",
+ "recordType": "TXT",
+ "publicKey": "BE9x3a2Qmock_prev_key_for_ui_preview_vd1n7x5k2c",
+ "publishTime": "2026-02-20 00:00:00",
+ "syncStatus": "success",
+ "nodesPending": 0,
+ "isCurrent": false,
+ },
+ }
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go
new file mode 100644
index 0000000..b4564e9
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go
@@ -0,0 +1,21 @@
+package ech
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "ech").
+ Prefix("/httpdns/ech").
+ Get("", new(IndexAction)).
+ Get("/audit", new(AuditAction)).
+ GetPost("/rollbackMfaPopup", new(RollbackMfaPopupAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go
new file mode 100644
index 0000000..4fc36a2
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go
@@ -0,0 +1,36 @@
+package ech
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+)
+
+type RollbackMfaPopupAction struct {
+ actionutils.ParentAction
+}
+
+func (this *RollbackMfaPopupAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *RollbackMfaPopupAction) RunGet(params struct {
+ LogId int64
+}) {
+ this.Data["logId"] = params.LogId
+ this.Show()
+}
+
+func (this *RollbackMfaPopupAction) RunPost(params struct {
+ LogId int64
+ Reason string
+ OtpCode1 string
+ OtpCode2 string
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("reason", params.Reason).Require("please input rollback reason")
+ params.Must.Field("otpCode1", params.OtpCode1).Require("please input first otp code")
+ params.Must.Field("otpCode2", params.OtpCode2).Require("please input second otp code")
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go
new file mode 100644
index 0000000..cc2b57c
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go
@@ -0,0 +1,66 @@
+package guide
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ httpdnsApps "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ httpdnsPolicies "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "guide", "")
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+
+ apps := []maps.Map{
+ {
+ "id": int64(1),
+ "name": "主站移动业务",
+ "appId": "ab12xc34s2",
+ "clusterId": int64(1),
+ },
+ {
+ "id": int64(2),
+ "name": "视频网关业务",
+ "appId": "vd8992ksm1",
+ "clusterId": int64(2),
+ },
+ {
+ "id": int64(3),
+ "name": "海外灰度测试",
+ "appId": "ov7711hkq9",
+ "clusterId": int64(1),
+ },
+ }
+
+ for _, app := range apps {
+ clusterID := app.GetInt64("clusterId")
+ app["gatewayDomain"] = httpdnsPolicies.LoadClusterGatewayByID(clusterID)
+
+ settings := httpdnsApps.LoadAppSettingsByAppID(app.GetString("appId"))
+ if settings == nil {
+ app["signSecret"] = ""
+ app["signSecretMasked"] = ""
+ app["aesSecret"] = ""
+ app["aesSecretMasked"] = ""
+ app["signEnabled"] = false
+ continue
+ }
+
+ app["signSecret"] = settings.GetString("signSecretPlain")
+ app["signSecretMasked"] = settings.GetString("signSecretMasked")
+ app["aesSecret"] = settings.GetString("aesSecretPlain")
+ app["aesSecretMasked"] = settings.GetString("aesSecretMasked")
+ app["signEnabled"] = settings.GetBool("signEnabled")
+ }
+
+ this.Data["apps"] = apps
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go
new file mode 100644
index 0000000..efccb9d
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go
@@ -0,0 +1,19 @@
+package guide
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "guide").
+ Prefix("/httpdns/guide").
+ Get("", new(IndexAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go b/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go
new file mode 100644
index 0000000..9d5ce32
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go
@@ -0,0 +1,49 @@
+package httpdnsutils
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+func AddLeftMenu(action *actionutils.ParentAction) {
+ tab := action.Data.GetString("mainTab")
+ action.Data["teaMenu"] = "httpdns"
+ action.Data["teaSubMenu"] = tab
+ action.Data["leftMenuItems"] = []maps.Map{
+ {
+ "name": "集群管理",
+ "url": "/httpdns/clusters",
+ "isActive": tab == "cluster",
+ },
+ {
+ "name": "全局配置",
+ "url": "/httpdns/policies",
+ "isActive": tab == "policy",
+ },
+ {
+ "name": "应用管理",
+ "url": "/httpdns/apps",
+ "isActive": tab == "app",
+ },
+ {
+ "name": "SDK接入引导",
+ "url": "/httpdns/guide",
+ "isActive": tab == "guide",
+ },
+ {
+ "name": "解析日志",
+ "url": "/httpdns/resolveLogs",
+ "isActive": tab == "resolveLogs",
+ },
+ {
+ "name": "运行日志",
+ "url": "/httpdns/runtimeLogs",
+ "isActive": tab == "runtimeLogs",
+ },
+ {
+ "name": "解析测试",
+ "url": "/httpdns/sandbox",
+ "isActive": tab == "sandbox",
+ },
+ }
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/index.go
new file mode 100644
index 0000000..df37453
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/index.go
@@ -0,0 +1,11 @@
+package httpdns
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ this.RedirectURL("/httpdns/clusters")
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/init.go
new file mode 100644
index 0000000..aabd270
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/init.go
@@ -0,0 +1,19 @@
+package httpdns
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "cluster").
+ Prefix("/httpdns").
+ Get("", new(IndexAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go
new file mode 100644
index 0000000..9ac3052
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go
@@ -0,0 +1,68 @@
+package policies
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "policy", "")
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["policies"] = loadGlobalPolicies()
+ this.Data["availableClusters"] = loadAvailableDeployClusters()
+ this.Show()
+}
+
+func (this *IndexAction) RunPost(params struct {
+ DefaultClusterId int64
+ EnableUserDomainVerify bool
+ DefaultTTL int
+ DefaultFallbackMs int
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ if params.DefaultClusterId <= 0 || !isValidClusterID(params.DefaultClusterId) {
+ this.Fail("please select a valid default cluster")
+ return
+ }
+
+ params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "default ttl should be > 0")
+ params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "default fallback should be > 0")
+
+ if params.DefaultTTL > 86400 {
+ this.Fail("default TTL should be <= 86400")
+ return
+ }
+ if params.DefaultFallbackMs > 10000 {
+ this.Fail("default fallback should be <= 10000 ms")
+ return
+ }
+
+ saveGlobalPolicies(maps.Map{
+ "defaultClusterId": params.DefaultClusterId,
+ "enableUserDomainVerify": params.EnableUserDomainVerify,
+ "defaultTTL": params.DefaultTTL,
+ "defaultFallbackMs": params.DefaultFallbackMs,
+ })
+
+ this.Success()
+}
+
+func isValidClusterID(clusterID int64) bool {
+ for _, cluster := range loadAvailableDeployClusters() {
+ if cluster.GetInt64("id") == clusterID {
+ return true
+ }
+ }
+ return false
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go
new file mode 100644
index 0000000..cef7fcf
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go
@@ -0,0 +1,19 @@
+package policies
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "policy").
+ Prefix("/httpdns/policies").
+ GetPost("", new(IndexAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
new file mode 100644
index 0000000..68cc1e0
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
@@ -0,0 +1,172 @@
+package policies
+
+import (
+ "strings"
+ "sync"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+var globalPoliciesStore = struct {
+ sync.RWMutex
+ data maps.Map
+ publicSniPools []maps.Map
+ publicSniCertificates []maps.Map
+}{
+ data: maps.Map{
+ "defaultClusterId": int64(1),
+ "enableUserDomainVerify": true,
+ "defaultTTL": 30,
+ "defaultFallbackMs": 300,
+ },
+ publicSniPools: []maps.Map{
+ {
+ "domain": "public-sni-a.waf.example.com",
+ "certId": "cert_public_sni_a",
+ },
+ {
+ "domain": "public-sni-b.waf.example.com",
+ "certId": "cert_public_sni_b",
+ },
+ },
+ publicSniCertificates: []maps.Map{
+ {
+ "id": "cert_public_sni_a",
+ "name": "public-sni-a.waf.example.com",
+ "issuer": "Mock CA",
+ "expiresAt": "2026-12-31 23:59:59",
+ },
+ {
+ "id": "cert_public_sni_b",
+ "name": "public-sni-b.waf.example.com",
+ "issuer": "Mock CA",
+ "expiresAt": "2027-03-31 23:59:59",
+ },
+ },
+}
+
+func loadGlobalPolicies() maps.Map {
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+
+ return maps.Map{
+ "defaultClusterId": globalPoliciesStore.data.GetInt64("defaultClusterId"),
+ "enableUserDomainVerify": globalPoliciesStore.data.GetBool("enableUserDomainVerify"),
+ "defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
+ "defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
+ }
+}
+
+func saveGlobalPolicies(policies maps.Map) {
+ globalPoliciesStore.Lock()
+ globalPoliciesStore.data = maps.Map{
+ "defaultClusterId": policies.GetInt64("defaultClusterId"),
+ "enableUserDomainVerify": policies.GetBool("enableUserDomainVerify"),
+ "defaultTTL": policies.GetInt("defaultTTL"),
+ "defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
+ }
+
+ globalPoliciesStore.Unlock()
+}
+
+func loadPublicSNICertificates() []maps.Map {
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+
+ return cloneMapSlice(globalPoliciesStore.publicSniCertificates)
+}
+
+// LoadPublicSNICertificates returns global public SNI certificates for other httpdns modules.
+func LoadPublicSNICertificates() []maps.Map {
+ return loadPublicSNICertificates()
+}
+
+func loadAvailableDeployClusters() []maps.Map {
+ return []maps.Map{
+ {
+ "id": int64(1),
+ "name": "gateway-cn-hz",
+ "region": "cn-hangzhou",
+ "gatewayDomain": "gw-hz.httpdns.example.com",
+ },
+ {
+ "id": int64(2),
+ "name": "gateway-cn-bj",
+ "region": "cn-beijing",
+ "gatewayDomain": "gw-bj.httpdns.example.com",
+ },
+ }
+}
+
+// LoadAvailableDeployClusters returns selectable clusters for HTTPDNS user defaults.
+func LoadAvailableDeployClusters() []maps.Map {
+ return loadAvailableDeployClusters()
+}
+
+// LoadDefaultClusterID returns current default deploy cluster id for HTTPDNS users.
+func LoadDefaultClusterID() int64 {
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+ return globalPoliciesStore.data.GetInt64("defaultClusterId")
+}
+
+// LoadClusterGatewayByID returns gateway domain for the selected cluster.
+func LoadClusterGatewayByID(clusterID int64) string {
+ clusters := loadAvailableDeployClusters()
+ for _, cluster := range clusters {
+ if cluster.GetInt64("id") == clusterID {
+ gatewayDomain := strings.TrimSpace(cluster.GetString("gatewayDomain"))
+ if len(gatewayDomain) > 0 {
+ return gatewayDomain
+ }
+ }
+ }
+
+ if len(clusters) > 0 {
+ fallback := strings.TrimSpace(clusters[0].GetString("gatewayDomain"))
+ if len(fallback) > 0 {
+ return fallback
+ }
+ }
+
+ return "gw.httpdns.example.com"
+}
+
+// LoadPublicSNIPools returns global public SNI domain pool for other httpdns modules.
+func LoadPublicSNIPools() []maps.Map {
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+
+ return cloneMapSlice(globalPoliciesStore.publicSniPools)
+}
+
+// HasPublicSNIDomain checks if a public SNI domain exists in global pool.
+func HasPublicSNIDomain(domain string) bool {
+ domain = strings.ToLower(strings.TrimSpace(domain))
+ if len(domain) == 0 {
+ return false
+ }
+
+ globalPoliciesStore.RLock()
+ defer globalPoliciesStore.RUnlock()
+
+ for _, pool := range globalPoliciesStore.publicSniPools {
+ if strings.ToLower(strings.TrimSpace(pool.GetString("domain"))) == domain {
+ return true
+ }
+ }
+
+ return false
+}
+
+func cloneMapSlice(src []maps.Map) []maps.Map {
+ result := make([]maps.Map, 0, len(src))
+ for _, item := range src {
+ cloned := maps.Map{}
+ for k, v := range item {
+ cloned[k] = v
+ }
+ result = append(result, cloned)
+ }
+ return result
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go
new file mode 100644
index 0000000..e7e8505
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go
@@ -0,0 +1,151 @@
+package resolveLogs
+
+import (
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "resolveLogs", "")
+}
+
+func (this *IndexAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+ AppId string
+ Domain string
+ Status string
+ Keyword string
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["appId"] = params.AppId
+ this.Data["domain"] = params.Domain
+ this.Data["status"] = params.Status
+ this.Data["keyword"] = params.Keyword
+
+ clusters := []map[string]interface{}{
+ {"id": int64(1), "name": "gateway-cn-hz"},
+ {"id": int64(2), "name": "gateway-cn-bj"},
+ }
+ nodes := []map[string]interface{}{
+ {"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
+ {"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
+ {"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
+ }
+ this.Data["clusters"] = clusters
+ this.Data["nodes"] = nodes
+
+ allLogs := []map[string]interface{}{
+ {
+ "time": "2026-02-23 10:21:51",
+ "clusterId": int64(1),
+ "clusterName": "gateway-cn-hz",
+ "nodeId": int64(101),
+ "nodeName": "hz-node-01",
+ "appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
+ "appId": "ab12xc34s2",
+ "domain": "api.business.com",
+ "query": "A",
+ "clientIp": "203.0.113.25",
+ "os": "Android",
+ "sdkVersion": "2.4.1",
+ "ips": "198.51.100.10,198.51.100.11",
+ "status": "success",
+ "errorCode": "none",
+ "costMs": 37,
+ "pinningMode": "report",
+ "pinningResult": "warn",
+ "sanMode": "strict",
+ "sanResult": "pass",
+ },
+ {
+ "time": "2026-02-23 10:18:02",
+ "clusterId": int64(2),
+ "clusterName": "gateway-cn-bj",
+ "nodeId": int64(201),
+ "nodeName": "bj-node-01",
+ "appName": "\u652f\u4ed8\u7f51\u5173\u4e1a\u52a1",
+ "appId": "vd8992ksm1",
+ "domain": "payment.business.com",
+ "query": "A",
+ "clientIp": "198.51.100.67",
+ "os": "iOS",
+ "sdkVersion": "2.3.9",
+ "ips": "198.51.100.22",
+ "status": "failed",
+ "errorCode": "40301",
+ "costMs": 52,
+ "pinningMode": "enforce",
+ "pinningResult": "fail",
+ "sanMode": "strict",
+ "sanResult": "fail",
+ },
+ {
+ "time": "2026-02-23 10:12:44",
+ "clusterId": int64(1),
+ "clusterName": "gateway-cn-hz",
+ "nodeId": int64(102),
+ "nodeName": "hz-node-02",
+ "appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
+ "appId": "ab12xc34s2",
+ "domain": "www.aliyun.com",
+ "query": "A",
+ "clientIp": "106.11.63.180",
+ "os": "Unknown",
+ "sdkVersion": "none",
+ "ips": "3.33.120.190,15.197.120.33",
+ "status": "success",
+ "errorCode": "none",
+ "costMs": 41,
+ "pinningMode": "off",
+ "pinningResult": "na",
+ "sanMode": "report",
+ "sanResult": "warn",
+ },
+ }
+
+ status := strings.TrimSpace(strings.ToLower(params.Status))
+ appID := strings.TrimSpace(strings.ToLower(params.AppId))
+ domain := strings.TrimSpace(strings.ToLower(params.Domain))
+ keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
+
+ filtered := make([]map[string]interface{}, 0)
+ for _, log := range allLogs {
+ if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
+ continue
+ }
+ if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
+ continue
+ }
+ if len(status) > 0 && log["status"].(string) != status {
+ continue
+ }
+ if len(appID) > 0 && !strings.Contains(strings.ToLower(log["appId"].(string)), appID) {
+ continue
+ }
+ if len(domain) > 0 && !strings.Contains(strings.ToLower(log["domain"].(string)), domain) {
+ continue
+ }
+ if len(keyword) > 0 {
+ if !strings.Contains(strings.ToLower(log["appName"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["domain"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["clientIp"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["ips"].(string)), keyword) {
+ continue
+ }
+ }
+ filtered = append(filtered, log)
+ }
+
+ this.Data["resolveLogs"] = filtered
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go
new file mode 100644
index 0000000..da6afb4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go
@@ -0,0 +1,19 @@
+package resolveLogs
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "resolveLogs").
+ Prefix("/httpdns/resolveLogs").
+ Get("", new(IndexAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go
new file mode 100644
index 0000000..cd7b9b6
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go
@@ -0,0 +1,137 @@
+package runtimeLogs
+
+import (
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "runtimeLogs", "")
+}
+
+func (this *IndexAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+ DayFrom string
+ DayTo string
+ Level string
+ Keyword string
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["dayFrom"] = params.DayFrom
+ this.Data["dayTo"] = params.DayTo
+ this.Data["level"] = params.Level
+ this.Data["keyword"] = params.Keyword
+
+ clusters := []map[string]interface{}{
+ {"id": int64(1), "name": "gateway-cn-hz"},
+ {"id": int64(2), "name": "gateway-cn-bj"},
+ }
+ nodes := []map[string]interface{}{
+ {"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
+ {"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
+ {"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
+ }
+ this.Data["clusters"] = clusters
+ this.Data["nodes"] = nodes
+
+ allLogs := []map[string]interface{}{
+ {
+ "createdTime": "2026-02-23 10:30:12",
+ "clusterId": int64(1),
+ "clusterName": "gateway-cn-hz",
+ "nodeId": int64(101),
+ "nodeName": "hz-node-01",
+ "level": "info",
+ "tag": "config",
+ "module": "resolver",
+ "description": "\u914d\u7f6e\u70ed\u52a0\u8f7d\u5b8c\u6210\uff0c\u5df2\u542f\u7528\u6700\u65b0\u7b56\u7565\u5feb\u7167\u3002",
+ "count": int64(1),
+ "requestId": "rid-20260223-001",
+ },
+ {
+ "createdTime": "2026-02-23 10:27:38",
+ "clusterId": int64(2),
+ "clusterName": "gateway-cn-bj",
+ "nodeId": int64(201),
+ "nodeName": "bj-node-01",
+ "level": "warning",
+ "tag": "sni",
+ "module": "policy-engine",
+ "description": "\u68c0\u6d4b\u5230 level3 \u6761\u4ef6\u4e0d\u6ee1\u8db3\uff0c\u81ea\u52a8\u964d\u7ea7\u5230 level2\u3002",
+ "count": int64(3),
+ "requestId": "rid-20260223-002",
+ },
+ {
+ "createdTime": "2026-02-23 10:22:49",
+ "clusterId": int64(1),
+ "clusterName": "gateway-cn-hz",
+ "nodeId": int64(102),
+ "nodeName": "hz-node-02",
+ "level": "success",
+ "tag": "cache-refresh",
+ "module": "cache",
+ "description": "\u57df\u540d\u7f13\u5b58\u5237\u65b0\u4efb\u52a1\u5b8c\u6210\uff0c\u6210\u529f 2/2\u3002",
+ "count": int64(1),
+ "requestId": "rid-20260223-003",
+ },
+ {
+ "createdTime": "2026-02-23 10:18:11",
+ "clusterId": int64(1),
+ "clusterName": "gateway-cn-hz",
+ "nodeId": int64(101),
+ "nodeName": "hz-node-01",
+ "level": "error",
+ "tag": "upstream",
+ "module": "resolver",
+ "description": "\u4e0a\u6e38\u6743\u5a01 DNS \u8bf7\u6c42\u8d85\u65f6\uff0c\u89e6\u53d1\u91cd\u8bd5\u540e\u6062\u590d\u3002",
+ "count": int64(2),
+ "requestId": "rid-20260223-004",
+ },
+ }
+
+ keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
+ filtered := make([]map[string]interface{}, 0)
+ for _, log := range allLogs {
+ if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
+ continue
+ }
+ if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
+ continue
+ }
+ if len(params.Level) > 0 && log["level"].(string) != params.Level {
+ continue
+ }
+ if len(params.DayFrom) > 0 {
+ if log["createdTime"].(string)[:10] < params.DayFrom {
+ continue
+ }
+ }
+ if len(params.DayTo) > 0 {
+ if log["createdTime"].(string)[:10] > params.DayTo {
+ continue
+ }
+ }
+ if len(keyword) > 0 {
+ if !strings.Contains(strings.ToLower(log["tag"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["module"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["description"].(string)), keyword) &&
+ !strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) {
+ continue
+ }
+ }
+ filtered = append(filtered, log)
+ }
+
+ this.Data["runtimeLogs"] = filtered
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go
new file mode 100644
index 0000000..0816d7b
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go
@@ -0,0 +1,19 @@
+package runtimeLogs
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "runtimeLogs").
+ Prefix("/httpdns/runtimeLogs").
+ Get("", new(IndexAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go
new file mode 100644
index 0000000..62049e1
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go
@@ -0,0 +1,36 @@
+package sandbox
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+)
+
+type IndexAction struct {
+ actionutils.ParentAction
+}
+
+func (this *IndexAction) Init() {
+ this.Nav("httpdns", "sandbox", "")
+}
+
+func (this *IndexAction) RunGet(params struct{}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+ this.Data["apps"] = []map[string]interface{}{
+ {
+ "id": int64(1),
+ "name": "主站移动业务",
+ "appId": "ab12xc34s2",
+ },
+ {
+ "id": int64(2),
+ "name": "视频网关业务",
+ "appId": "vd8992ksm1",
+ },
+ {
+ "id": int64(3),
+ "name": "海外灰度测试",
+ "appId": "ov7711hkq9",
+ },
+ }
+ this.Show()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go
new file mode 100644
index 0000000..9a68b4e
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go
@@ -0,0 +1,20 @@
+package sandbox
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
+ "github.com/iwind/TeaGo"
+)
+
+func init() {
+ TeaGo.BeforeStart(func(server *TeaGo.Server) {
+ server.
+ Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
+ Data("teaMenu", "httpdns").
+ Data("teaSubMenu", "sandbox").
+ Prefix("/httpdns/sandbox").
+ Get("", new(IndexAction)).
+ Post("/test", new(TestAction)).
+ EndAll()
+ })
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
new file mode 100644
index 0000000..5811a0f
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
@@ -0,0 +1,163 @@
+package sandbox
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type TestAction struct {
+ actionutils.ParentAction
+}
+
+func (this *TestAction) RunPost(params struct {
+ AppId string
+ Domain string
+ ClientIp string
+ Qtype string
+ SDNSParamsJSON string
+}) {
+ if len(params.ClientIp) == 0 {
+ params.ClientIp = "203.0.113.100"
+ }
+ params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
+
+ sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+
+ clientSubnet := this.maskSubnet(params.ClientIp, 24, 56)
+ if len(clientSubnet) == 0 {
+ clientSubnet = "203.0.113.0/24"
+ }
+
+ sniPolicy := "empty"
+ publicSNI := ""
+ ecsMode := "off"
+ ecsIPv4Prefix := 24
+ ecsIPv6Prefix := 56
+ pinningMode := "off"
+ sanMode := "off"
+ query := url.Values{}
+ query.Set("appId", params.AppId)
+ query.Set("dn", params.Domain)
+ query.Set("cip", params.ClientIp)
+ query.Set("qtype", params.Qtype)
+ for _, item := range sdnsParams {
+ query.Add("sdns_"+item.GetString("name"), item.GetString("value"))
+ }
+ requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode()
+
+ this.Data["result"] = maps.Map{
+ "code": 0,
+ "message": "ok (mock)",
+ "requestId": "mock-rid-20260221-001",
+ "data": maps.Map{
+ "request_url": requestURL,
+ "client_ip": params.ClientIp,
+ "client_region": "中国, 上海, 上海",
+ "line_name": "默认线路",
+ "sdns_params": sdnsParams,
+ "ips": []string{"203.0.113.10", "203.0.113.11"},
+ "records": []maps.Map{
+ {
+ "domain": params.Domain,
+ "type": params.Qtype,
+ "ip": "203.0.113.10",
+ "ttl": 30,
+ "region": "中国-上海-上海",
+ "line": "默认线路",
+ },
+ {
+ "domain": params.Domain,
+ "type": params.Qtype,
+ "ip": "203.0.113.11",
+ "ttl": 30,
+ "region": "中国-上海-上海",
+ "line": "默认线路",
+ },
+ },
+ "ttl": 30,
+ "sni_policy": sniPolicy,
+ "public_sni": publicSNI,
+ "client_subnet": clientSubnet,
+ "ecs_mode": ecsMode,
+ "ecs_ipv4_prefix": ecsIPv4Prefix,
+ "ecs_ipv6_prefix": ecsIPv6Prefix,
+ "pinning_mode": pinningMode,
+ "san_mode": sanMode,
+ "fingerprint_algo": "sha256",
+ "cert_fingerprints": []string{"8f41ab8d32fca7f9a2b0fdd09a0e2ccf31e5579cd3a0b65f1a9f2f2ec8f38d4a"},
+ "verify_headers": maps.Map{
+ "X-Edge-Resolve-AppId": params.AppId,
+ "X-Edge-Resolve-Expire": "1768953600",
+ "X-Edge-Resolve-Nonce": "mock-nonce-123456",
+ "X-Edge-Resolve-Target": params.Domain,
+ "X-Edge-Resolve-Sign": "mock-signature-base64url",
+ },
+ },
+ }
+ this.Success()
+}
+
+func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
+ if len(raw) == 0 {
+ return []maps.Map{}, nil
+ }
+
+ list := []maps.Map{}
+ if err := json.Unmarshal([]byte(raw), &list); err != nil {
+ return nil, err
+ }
+ if len(list) > 10 {
+ return nil, fmt.Errorf("sdns params should be <= 10")
+ }
+
+ result := make([]maps.Map, 0, len(list))
+ for _, item := range list {
+ name := strings.TrimSpace(item.GetString("name"))
+ value := strings.TrimSpace(item.GetString("value"))
+ if len(name) == 0 && len(value) == 0 {
+ continue
+ }
+ if len(name) == 0 || len(name) > 64 {
+ return nil, fmt.Errorf("sdns param name should be in 1-64")
+ }
+ if len(value) == 0 || len(value) > 64 {
+ return nil, fmt.Errorf("sdns param value should be in 1-64")
+ }
+ result = append(result, maps.Map{
+ "name": name,
+ "value": value,
+ })
+ }
+
+ if len(result) > 10 {
+ return nil, fmt.Errorf("sdns params should be <= 10")
+ }
+ return result, nil
+}
+
+func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string {
+ parsed := net.ParseIP(ip)
+ if parsed == nil {
+ return ""
+ }
+
+ ipv4 := parsed.To4()
+ if ipv4 != nil {
+ mask := net.CIDRMask(ipv4Prefix, 32)
+ return ipv4.Mask(mask).String() + "/" + strconv.Itoa(ipv4Prefix)
+ }
+
+ mask := net.CIDRMask(ipv6Prefix, 128)
+ return parsed.Mask(mask).String() + "/" + strconv.Itoa(ipv6Prefix)
+}
diff --git a/EdgeAdmin/internal/web/helpers/menu.go b/EdgeAdmin/internal/web/helpers/menu.go
index 5712079..e7a3108 100644
--- a/EdgeAdmin/internal/web/helpers/menu.go
+++ b/EdgeAdmin/internal/web/helpers/menu.go
@@ -113,6 +113,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
+ {
+ "code": "httpdns",
+ "module": configloaders.AdminModuleCodeHttpDNS,
+ "name": "HTTPDNS",
+ "subtitle": "",
+ "icon": "shield alternate",
+ "subItems": []maps.Map{
+ {
+ "name": "集群管理",
+ "url": "/httpdns/clusters",
+ "code": "cluster",
+ },
+ {
+ "name": "全局配置",
+ "url": "/httpdns/policies",
+ "code": "policy",
+ },
+ {
+ "name": "应用管理",
+ "url": "/httpdns/apps",
+ "code": "app",
+ },
+ {
+ "name": "SDK接入引导",
+ "url": "/httpdns/guide",
+ "code": "guide",
+ },
+ {
+ "name": "解析日志",
+ "url": "/httpdns/resolveLogs",
+ "code": "resolveLogs",
+ },
+ {
+ "name": "运行日志",
+ "url": "/httpdns/runtimeLogs",
+ "code": "runtimeLogs",
+ },
+ {
+ "name": "解析测试",
+ "url": "/httpdns/sandbox",
+ "code": "sandbox",
+ },
+ },
+ },
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
diff --git a/EdgeAdmin/internal/web/helpers/menu_plus.go b/EdgeAdmin/internal/web/helpers/menu_plus.go
index 9e40652..c792b6d 100644
--- a/EdgeAdmin/internal/web/helpers/menu_plus.go
+++ b/EdgeAdmin/internal/web/helpers/menu_plus.go
@@ -178,6 +178,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
+ {
+ "code": "httpdns",
+ "module": configloaders.AdminModuleCodeHttpDNS,
+ "name": "HTTPDNS",
+ "subtitle": "",
+ "icon": "shield alternate",
+ "subItems": []maps.Map{
+ {
+ "name": "集群管理",
+ "url": "/httpdns/clusters",
+ "code": "cluster",
+ },
+ {
+ "name": "全局配置",
+ "url": "/httpdns/policies",
+ "code": "policy",
+ },
+ {
+ "name": "应用管理",
+ "url": "/httpdns/apps",
+ "code": "app",
+ },
+ {
+ "name": "SDK接入引导",
+ "url": "/httpdns/guide",
+ "code": "guide",
+ },
+ {
+ "name": "解析日志",
+ "url": "/httpdns/resolveLogs",
+ "code": "resolveLogs",
+ },
+ {
+ "name": "运行日志",
+ "url": "/httpdns/runtimeLogs",
+ "code": "runtimeLogs",
+ },
+ {
+ "name": "解析测试",
+ "url": "/httpdns/sandbox",
+ "code": "sandbox",
+ },
+ },
+ },
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
diff --git a/EdgeAdmin/internal/web/import.go b/EdgeAdmin/internal/web/import.go
index 6cd88b9..10f2c89 100644
--- a/EdgeAdmin/internal/web/import.go
+++ b/EdgeAdmin/internal/web/import.go
@@ -136,4 +136,15 @@ import (
// 平台用户
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users"
+
+ // HTTPDNS体系
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/ech"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/guide"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox"
)
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
new file mode 100644
index 0000000..3e06600
--- /dev/null
+++ b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
@@ -0,0 +1,4 @@
+
| 线路 | +规则名称 | +SDNS 参数 | +解析记录 | +TTL | +状态 | +操作 | +
|---|---|---|---|---|---|---|
| {{record.lineText}} | +{{record.ruleName}} | +{{record.paramsText}} | ++ {{record.recordValueText}} + | +{{record.ttl}}s | ++ 已启用 + 已停用 + | ++ 编辑 | + {{record.isOn ? "停用" : "启用"}} | + 删除 + | +
| 域名列表 | +规则策略 | +操作 | +
|---|---|---|
| {{domain.name}} | ++ {{domain.customRecordCount}} + | ++ 自定义解析 | + 解绑 + | +
| 应用名称 | +AppID | +绑定域名数 | +状态 | +操作 | +
|---|---|---|---|---|
|
+
+ |
+
+ {{app.appId}}
+ |
+ {{app.domainCount}} | +
+ |
+ + 域名管理 | + 应用设置 + | +
当前集群下暂时还没有节点。
+ +| 节点名称 | +IP地址 | +状态 | +操作 | +
|---|---|---|---|
|
+ {{node.name}}
+
+
+ {{node.region.name}}
+
+ |
+
+ -
+
+
+ {{addr.ip}}
+ ({{addr.name}})
+ [内]
+ [下线]
+ [宕机]
+
+ |
+
+
+ 宕机
+
+
+ 未启用
+
+
+ 在线
+ 离线
+
+ |
+ + 详情 + 删除 + | +
| 节点名称 | +{{node.name}} | +
| 状态 | +|
| IP地址 | +
+
+
+
+
+
+ {{address.ip}}
+ ({{address.name}},不公开访问)
+ [off]
+ [down]
+ (不公开访问)
+
+
+ 暂时还没有填写 IP 地址。
+
+ |
+
| SSH主机地址 | +
+
+ {{node.login.params.host}}
+ 尚未设置
+
+ 尚未设置
+ |
+
| SSH主机端口 | +
+
+ {{node.login.params.port}}
+ 尚未设置
+
+ 尚未设置
+ |
+
| SSH登录认证 | +
+
+
+ {{node.login.grant.name}}
+ ({{node.login.grant.methodName}})
+ ({{node.login.grant.username}})
+
+
+ 尚未设置
+ |
+
| API节点地址 | +
+
+ {{addr}}
+
+ 使用全局设置
+ |
+
| 运行状态 | ++ + + | +
| CPU用量 | ++ {{node.status.cpuUsageText}} + + ({{node.status.cpuPhysicalCount}}核心/{{node.status.cpuLogicalCount}}线程) + | +
| 内存用量 | +{{node.status.memUsageText}} | +
| 负载 | +
+ {{node.status.load1m}} {{node.status.load5m}} {{node.status.load15m}}
+
+ |
+
| 版本 | ++ v{{node.status.buildVersion}} + + 发现新版本 v{{newVersion}} » + | +
| 主程序位置 | +{{node.status.exePath}} | +
| 最近API连接状况 | ++ + 连接错误异常严重({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + 连接错误较多({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + 有连接错误发生({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + + 连接良好 + 连接基本稳定(平均{{round(node.status.apiAvgCostSeconds)}}秒) + 连接速度较慢(平均{{round(node.status.apiAvgCostSeconds)}}秒) + 连接非常慢(平均{{round(node.status.apiAvgCostSeconds)}}秒),请改善当前节点和API节点之间通讯 + + + 尚未上报数据 + | +
| 上次更新时间 | +
+ {{nodeDatetime}}
+ 当前节点时间与API节点时间相差 {{nodeTimeDiff}} 秒,请同步节点时间。 + |
+
每隔30秒钟更新一次运行状态。
+ + +| 节点ID (id) | +{{node.uniqueId}} | +
| 密钥 (secret) | +{{node.secret}} | +
| 安装目录 | +
+ 使用集群设置({{node.cluster.installDir}})
+ {{node.installDir}}
+ |
+
| 已安装 | ++ 已安装 + 未安装 + | +
| 配置文件 | +
+ configs/api_httpdns.yaml
+ |
+
| 配置内容 | +
+ 每个节点的配置文件内容均不相同,不能混用。 + |
+
| 安装目录 | +
+ 使用集群设置({{node.cluster.installDir}})
+
+ {{node.installDir}}
+ |
+
| SSH地址 | ++ {{sshAddr}} [修改] + 尚未设置 [设置] + | +
| 配置文件 | +
+ configs/api_httpdns.yaml
+ |
+
| 配置内容 | +
+ |
+
| 安装目录 | +
+ 使用集群设置({{node.cluster.installDir}})
+
+ {{node.installDir}}
+ |
+
暂时还没有日志。
+ +|
+ |
+
{{cluster.name}}
+ + diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js new file mode 100644 index 0000000..5cfac1e --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js @@ -0,0 +1,3 @@ +Tea.context(function () { + this.success = NotifyReloadSuccess("保存成功"); +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/create.html b/EdgeAdmin/web/views/@default/httpdns/clusters/create.html new file mode 100644 index 0000000..d112bbe --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/create.html @@ -0,0 +1,23 @@ +{$layout} +{$template "menu"} + + + + + diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css new file mode 100644 index 0000000..e2dadd6 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css @@ -0,0 +1,7 @@ +.left-box { + top: 10em; +} +.right-box { + top: 10em; +} +/*# sourceMappingURL=createNode.css.map */ \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map new file mode 100644 index 0000000..fd804f1 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["createNode.less"],"names":[],"mappings":"AAAA;EACC,SAAA;;AAGD;EACC,SAAA","file":"createNode.css"} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html new file mode 100644 index 0000000..a5be9b9 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html @@ -0,0 +1,22 @@ +{$layout} +{$template "cluster_menu"} + + \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js new file mode 100644 index 0000000..0b4baf7 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js @@ -0,0 +1,3 @@ +Tea.context(function () { + this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster?clusterId=" + this.clusterId); +}); \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less new file mode 100644 index 0000000..f54837f --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less @@ -0,0 +1,7 @@ +.left-box { + top: 10em; +} + +.right-box { + top: 10em; +} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html new file mode 100644 index 0000000..fa5dd75 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html @@ -0,0 +1,7 @@ +{$layout} +{$template "cluster_menu"} + + + \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js new file mode 100644 index 0000000..d8d3cfb --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js @@ -0,0 +1,16 @@ +Tea.context(function () { + this.deleteCluster = function (clusterId) { + let that = this + teaweb.confirm("确定要删除此集群吗?", function () { + that.$post("/httpdns/clusters/delete") + .params({ + clusterId: clusterId + }) + .success(function () { + teaweb.success("删除成功", function () { + window.location = "/httpdns/clusters" + }) + }) + }) + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/index.html b/EdgeAdmin/web/views/@default/httpdns/clusters/index.html new file mode 100644 index 0000000..bbf124c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/index.html @@ -0,0 +1,68 @@ +{$layout} +{$template "menu"} + +| 集群名称 | +服务域名 | +节点数 | +在线节点数 | +状态 | +操作 | +
|---|---|---|---|---|---|
|
+
+ |
+
+ {{cluster.gatewayDomain}}
+ |
+ + + {{cluster.countAllNodes}} + + - + | ++ + {{cluster.countActiveNodes}} + + - + | ++ 详情 + | +
| 发行版本号 | +记录分发类型 | +公钥 | +发布时间 | +边缘扩散状态 | +受控回落通道 | +
|---|---|---|---|---|---|
| {{log.version}}当前生效 | +{{log.recordType}} | +
+
+ {{log.publicKey.substring(0,30)}}...{{log.publicKey.substring(log.publicKey.length-10)}}
+
+ |
+ {{log.publishTime}} | ++ 100% 同步 + 正在扩散到 {{log.nodesPending}} + 节点 + 部分节点超时 + | ++ 回滚降级 + (MFA验证) + - + | +
请先选择应用,然后查看配置并完成 SDK 接入。
+选择对应平台 SDK 下载并查阅集成文档。
+ + +暂时还没有解析日志。
+ +| 集群 | +节点 | +域名 | +类型 | +概要 | +
|---|---|---|---|---|
| {{log.clusterName}} | +{{log.nodeName}} | +{{log.domain}} | +{{log.query}} | +
+
+ {{log.time}}
+ | {{log.appName}}(
+ {{log.appId}})
+ | {{log.clientIp}}
+ | {{log.os}}/{{log.sdkVersion}}
+ | {{log.query}} {{log.domain}} ->
+ {{log.ips}}
+ [无记录]
+ |
+ 成功
+ 失败
+ ({{log.errorCode}})
+ | {{log.costMs}}ms
+ |
+
暂时还没有运行日志。
+ +| 时间 | +集群 | +节点 | +级别 | +类型 | +模块 | +详情 | +次数 | +请求ID | +
|---|---|---|---|---|---|---|---|---|
| {{log.createdTime}} | +{{log.clusterName}} | +{{log.nodeName}} | ++ error + warning + info + success + | +{{log.tag}} |
+ {{log.module}} | +{{log.description}} | +{{log.count}} | +{{log.requestId}} |
+
用于测试 ECS 掩码与区域调度效果。
+请在左侧配置参数后点击「在线解析」。
+{{response.data.request_url || '-'}}
+ {{response.data.client_ip || request.clientIp || '-'}}| 解析域名 | +解析类型 | +IP地址 | +TTL | +地区 | +线路 | +
|---|---|---|---|---|---|
| {{row.domain || request.domain}} | +{{row.type || request.qtype}} | +{{row.ip}} |
+ {{row.ttl}}s | +{{row.region || '-'}} | +{{row.line || '-'}} | +