更新UI
This commit is contained in:
@@ -3,26 +3,57 @@ package config
|
||||
import "strings"
|
||||
|
||||
type SafeBrandingConfig struct {
|
||||
SiteName string `json:"siteName"`
|
||||
PortalTitle string `json:"portalTitle"`
|
||||
PortalSubtitle string `json:"portalSubtitle"`
|
||||
AdminTitle string `json:"adminTitle"`
|
||||
AdminSubtitle string `json:"adminSubtitle"`
|
||||
SiteIconURL string `json:"siteIconUrl"`
|
||||
LogoURL string `json:"logoUrl"`
|
||||
DeveloperAvatarURL string `json:"developerAvatarUrl"`
|
||||
DeveloperName string `json:"developerName"`
|
||||
FeedbackEmail string `json:"feedbackEmail"`
|
||||
}
|
||||
|
||||
func SafeBranding(cfg BrandingConfig) SafeBrandingConfig {
|
||||
normalized := NormalizeBranding(BrandingConfig{}, cfg)
|
||||
return SafeBrandingConfig{
|
||||
SiteIconURL: strings.TrimSpace(cfg.SiteIconURL),
|
||||
DeveloperAvatarURL: strings.TrimSpace(cfg.DeveloperAvatarURL),
|
||||
DeveloperName: strings.TrimSpace(firstNonEmpty(cfg.DeveloperName, "YMhut")),
|
||||
FeedbackEmail: strings.TrimSpace(firstNonEmpty(cfg.FeedbackEmail, "support@ymhut.cn")),
|
||||
SiteName: strings.TrimSpace(normalized.SiteName),
|
||||
PortalTitle: strings.TrimSpace(normalized.PortalTitle),
|
||||
PortalSubtitle: strings.TrimSpace(normalized.PortalSubtitle),
|
||||
AdminTitle: strings.TrimSpace(normalized.AdminTitle),
|
||||
AdminSubtitle: strings.TrimSpace(normalized.AdminSubtitle),
|
||||
SiteIconURL: strings.TrimSpace(normalized.SiteIconURL),
|
||||
LogoURL: strings.TrimSpace(normalized.LogoURL),
|
||||
DeveloperAvatarURL: strings.TrimSpace(normalized.DeveloperAvatarURL),
|
||||
DeveloperName: strings.TrimSpace(normalized.DeveloperName),
|
||||
FeedbackEmail: strings.TrimSpace(normalized.FeedbackEmail),
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeBranding(current BrandingConfig, incoming BrandingConfig) BrandingConfig {
|
||||
next := current
|
||||
if value := strings.TrimSpace(incoming.SiteName); value != "" {
|
||||
next.SiteName = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.PortalTitle); value != "" {
|
||||
next.PortalTitle = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.PortalSubtitle); value != "" {
|
||||
next.PortalSubtitle = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.AdminTitle); value != "" {
|
||||
next.AdminTitle = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.AdminSubtitle); value != "" {
|
||||
next.AdminSubtitle = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.SiteIconURL); value != "" {
|
||||
next.SiteIconURL = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.LogoURL); value != "" {
|
||||
next.LogoURL = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.DeveloperAvatarURL); value != "" {
|
||||
next.DeveloperAvatarURL = value
|
||||
}
|
||||
@@ -32,9 +63,27 @@ func NormalizeBranding(current BrandingConfig, incoming BrandingConfig) Branding
|
||||
if value := strings.TrimSpace(incoming.FeedbackEmail); value != "" {
|
||||
next.FeedbackEmail = value
|
||||
}
|
||||
if next.SiteName == "" {
|
||||
next.SiteName = "YMhut Box"
|
||||
}
|
||||
if next.PortalTitle == "" {
|
||||
next.PortalTitle = next.SiteName + " 统一服务门户"
|
||||
}
|
||||
if next.PortalSubtitle == "" {
|
||||
next.PortalSubtitle = "统一发布、反馈与接口源状态门户"
|
||||
}
|
||||
if next.AdminTitle == "" {
|
||||
next.AdminTitle = "YMhut 统一管理台"
|
||||
}
|
||||
if next.AdminSubtitle == "" {
|
||||
next.AdminSubtitle = "发布、反馈、接口源与系统运维"
|
||||
}
|
||||
if next.SiteIconURL == "" {
|
||||
next.SiteIconURL = "/assets/favicon.ico"
|
||||
}
|
||||
if next.LogoURL == "" {
|
||||
next.LogoURL = next.SiteIconURL
|
||||
}
|
||||
if next.DeveloperAvatarURL == "" {
|
||||
next.DeveloperAvatarURL = "/assets/developer-avatar.png"
|
||||
}
|
||||
|
||||
@@ -72,7 +72,13 @@ type MailConfig struct {
|
||||
}
|
||||
|
||||
type BrandingConfig struct {
|
||||
SiteName string `json:"site_name"`
|
||||
PortalTitle string `json:"portal_title"`
|
||||
PortalSubtitle string `json:"portal_subtitle"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
AdminSubtitle string `json:"admin_subtitle"`
|
||||
SiteIconURL string `json:"site_icon_url"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
DeveloperAvatarURL string `json:"developer_avatar_url"`
|
||||
DeveloperName string `json:"developer_name"`
|
||||
FeedbackEmail string `json:"feedback_email"`
|
||||
@@ -163,7 +169,13 @@ func defaults(root string) *Config {
|
||||
TimeoutSeconds: 20,
|
||||
},
|
||||
Branding: BrandingConfig{
|
||||
SiteName: "YMhut Box",
|
||||
PortalTitle: "YMhut Box 统一服务门户",
|
||||
PortalSubtitle: "统一发布、反馈与接口源状态门户",
|
||||
AdminTitle: "YMhut 统一管理台",
|
||||
AdminSubtitle: "发布、反馈、接口源与系统运维",
|
||||
SiteIconURL: "/assets/favicon.ico",
|
||||
LogoURL: "/assets/favicon.ico",
|
||||
DeveloperAvatarURL: "/assets/developer-avatar.png",
|
||||
DeveloperName: "YMhut",
|
||||
FeedbackEmail: "support@ymhut.cn",
|
||||
@@ -276,6 +288,24 @@ func applyEnv(cfg *Config) {
|
||||
if value := os.Getenv("YMHUT_BRAND_ICON_URL"); value != "" {
|
||||
cfg.Branding.SiteIconURL = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_SITE_NAME"); value != "" {
|
||||
cfg.Branding.SiteName = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_PORTAL_TITLE"); value != "" {
|
||||
cfg.Branding.PortalTitle = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_PORTAL_SUBTITLE"); value != "" {
|
||||
cfg.Branding.PortalSubtitle = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_ADMIN_TITLE"); value != "" {
|
||||
cfg.Branding.AdminTitle = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_ADMIN_SUBTITLE"); value != "" {
|
||||
cfg.Branding.AdminSubtitle = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_LOGO_URL"); value != "" {
|
||||
cfg.Branding.LogoURL = value
|
||||
}
|
||||
if value := os.Getenv("YMHUT_BRAND_DEVELOPER_AVATAR_URL"); value != "" {
|
||||
cfg.Branding.DeveloperAvatarURL = value
|
||||
}
|
||||
@@ -453,9 +483,27 @@ func normalize(root string, cfg *Config) {
|
||||
if cfg.Mail.TimeoutSeconds <= 0 {
|
||||
cfg.Mail.TimeoutSeconds = 20
|
||||
}
|
||||
if cfg.Branding.SiteName == "" {
|
||||
cfg.Branding.SiteName = "YMhut Box"
|
||||
}
|
||||
if cfg.Branding.PortalTitle == "" {
|
||||
cfg.Branding.PortalTitle = cfg.Branding.SiteName + " 统一服务门户"
|
||||
}
|
||||
if cfg.Branding.PortalSubtitle == "" {
|
||||
cfg.Branding.PortalSubtitle = "统一发布、反馈与接口源状态门户"
|
||||
}
|
||||
if cfg.Branding.AdminTitle == "" {
|
||||
cfg.Branding.AdminTitle = "YMhut 统一管理台"
|
||||
}
|
||||
if cfg.Branding.AdminSubtitle == "" {
|
||||
cfg.Branding.AdminSubtitle = "发布、反馈、接口源与系统运维"
|
||||
}
|
||||
if cfg.Branding.SiteIconURL == "" {
|
||||
cfg.Branding.SiteIconURL = "/assets/favicon.ico"
|
||||
}
|
||||
if cfg.Branding.LogoURL == "" {
|
||||
cfg.Branding.LogoURL = cfg.Branding.SiteIconURL
|
||||
}
|
||||
if cfg.Branding.DeveloperAvatarURL == "" {
|
||||
cfg.Branding.DeveloperAvatarURL = "/assets/developer-avatar.png"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
healthCounts, _ := s.groupCounts("source_endpoints", "last_status")
|
||||
recentChecks, _ := s.RecentSourceChecks(limit)
|
||||
recentCalls, _ := s.RecentSourceCalls(limit)
|
||||
averageLatency, _ := s.AverageSourceLatencyBuckets(limit)
|
||||
audit, _ := s.ListAuditLogs(10)
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
@@ -34,12 +35,94 @@ func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
"feedbackStatus": statusCounts,
|
||||
"sourceHealth": healthCounts,
|
||||
"heartbeats": recentChecks,
|
||||
"averageLatency": averageLatency,
|
||||
"clientCalls": recentCalls,
|
||||
"database": s.Status(),
|
||||
"audit": audit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) AverageSourceLatencyBuckets(limit int) ([]map[string]any, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 80
|
||||
}
|
||||
rows, err := s.query(`SELECT checked_at, latency_ms, status FROM endpoint_health_checks ORDER BY checked_at DESC, id DESC LIMIT ?`, limit*4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
type bucket struct {
|
||||
label string
|
||||
total int
|
||||
count int
|
||||
ok int
|
||||
latest string
|
||||
}
|
||||
order := []string{}
|
||||
buckets := map[string]*bucket{}
|
||||
for rows.Next() {
|
||||
var checkedAt, status string
|
||||
var latency int
|
||||
if err := rows.Scan(&checkedAt, &latency, &status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label := latencyBucketLabel(checkedAt)
|
||||
if label == "" {
|
||||
label = checkedAt
|
||||
}
|
||||
item, ok := buckets[label]
|
||||
if !ok {
|
||||
item = &bucket{label: label, latest: checkedAt}
|
||||
buckets[label] = item
|
||||
order = append(order, label)
|
||||
}
|
||||
item.total += latency
|
||||
item.count++
|
||||
if status == "ok" || status == "redirected" {
|
||||
item.ok++
|
||||
}
|
||||
if checkedAt > item.latest {
|
||||
item.latest = checkedAt
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []map[string]any{}
|
||||
for i := len(order) - 1; i >= 0; i-- {
|
||||
item := buckets[order[i]]
|
||||
if item == nil || item.count == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"label": item.label,
|
||||
"averageLatency": item.total / item.count,
|
||||
"avgLatencyMs": item.total / item.count,
|
||||
"sampleCount": item.count,
|
||||
"healthyCount": item.ok,
|
||||
"checkedAt": item.latest,
|
||||
})
|
||||
}
|
||||
if len(out) > limit {
|
||||
out = out[len(out)-limit:]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func latencyBucketLabel(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
if len(value) >= 16 {
|
||||
return value[:16]
|
||||
}
|
||||
return value
|
||||
}
|
||||
return parsed.UTC().Format("01-02 15:04")
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceChecks(limit int) ([]map[string]any, error) {
|
||||
rows, err := s.query(`SELECT h.id, h.source_db_id, COALESCE(e.source_id, ''), COALESCE(e.name, ''), h.status, h.latency_ms, h.error, h.checked_at
|
||||
FROM endpoint_health_checks h LEFT JOIN source_endpoints e ON e.id = h.source_db_id
|
||||
|
||||
@@ -3,6 +3,8 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Store) UpsertSource(item Source) (Source, error) {
|
||||
@@ -165,6 +167,65 @@ func (s *Store) RecordSourceCheck(sourceDBID int64, status string, latency int,
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) SourceHealthHistory(sourceDBIDs []int64, limit int) (map[int64][]map[string]any, error) {
|
||||
if len(sourceDBIDs) == 0 {
|
||||
return map[int64][]map[string]any{}, nil
|
||||
}
|
||||
if limit <= 0 || limit > 48 {
|
||||
limit = 16
|
||||
}
|
||||
placeholders := make([]string, 0, len(sourceDBIDs))
|
||||
args := make([]any, 0, len(sourceDBIDs)+1)
|
||||
for _, id := range sourceDBIDs {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, id)
|
||||
}
|
||||
if len(placeholders) == 0 {
|
||||
return map[int64][]map[string]any{}, nil
|
||||
}
|
||||
args = append(args, limit*len(placeholders))
|
||||
rows, err := s.query(fmt.Sprintf(`SELECT source_db_id, status, latency_ms, checked_at
|
||||
FROM endpoint_health_checks
|
||||
WHERE source_db_id IN (%s)
|
||||
ORDER BY checked_at DESC, id DESC LIMIT ?`, strings.Join(placeholders, ",")), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[int64][]map[string]any{}
|
||||
for rows.Next() {
|
||||
var sourceDBID int64
|
||||
var status, checkedAt string
|
||||
var latency int
|
||||
if err := rows.Scan(&sourceDBID, &status, &latency, &checkedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out[sourceDBID]) >= limit {
|
||||
continue
|
||||
}
|
||||
out[sourceDBID] = append(out[sourceDBID], map[string]any{
|
||||
"status": status,
|
||||
"latencyMs": latency,
|
||||
"latency_ms": latency,
|
||||
"checkedAt": checkedAt,
|
||||
"checked_at": checkedAt,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for id, items := range out {
|
||||
for left, right := 0, len(items)-1; left < right; left, right = left+1, right-1 {
|
||||
items[left], items[right] = items[right], items[left]
|
||||
}
|
||||
out[id] = items
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) RecordSourceCall(call SourceCall) error {
|
||||
if call.CreatedAt == "" {
|
||||
call.CreatedAt = Now()
|
||||
|
||||
@@ -134,7 +134,7 @@ func (s *Service) Stop() {
|
||||
}
|
||||
|
||||
func (s *Service) loop() {
|
||||
ticker := time.NewTicker(time.Duration(s.cfg.SourceCheckSeconds) * time.Second)
|
||||
ticker := time.NewTicker(20 * time.Second)
|
||||
defer ticker.Stop()
|
||||
s.CheckDue(context.Background())
|
||||
for {
|
||||
@@ -269,6 +269,7 @@ func (s *Service) Catalog(includeHidden bool) (map[string]any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
histories, _ := s.store.SourceHealthHistory(sourceIDs(items), 16)
|
||||
categories := map[string]map[string]any{}
|
||||
for _, item := range items {
|
||||
cat, ok := categories[item.CategoryID]
|
||||
@@ -319,6 +320,7 @@ func (s *Service) Catalog(includeHidden bool) (map[string]any, error) {
|
||||
"lastError": item.LastError,
|
||||
"consecutiveFailure": item.ConsecutiveFailure,
|
||||
"meta": parseHealthMeta(item.LastError),
|
||||
"history": histories[item.ID],
|
||||
},
|
||||
}
|
||||
applyResolvedFields(sub, item.LastError)
|
||||
@@ -340,6 +342,7 @@ func (s *Service) Endpoints(includeHidden bool) ([]map[string]any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
histories, _ := s.store.SourceHealthHistory(sourceIDs(items), 16)
|
||||
out := []map[string]any{}
|
||||
for _, item := range items {
|
||||
var formats []string
|
||||
@@ -377,6 +380,7 @@ func (s *Service) Endpoints(includeHidden bool) ([]map[string]any, error) {
|
||||
"last_error": item.LastError,
|
||||
"consecutiveFailure": item.ConsecutiveFailure,
|
||||
"meta": parseHealthMeta(item.LastError),
|
||||
"history": histories[item.ID],
|
||||
},
|
||||
}
|
||||
applyResolvedFields(endpoint, item.LastError)
|
||||
@@ -385,23 +389,48 @@ func (s *Service) Endpoints(includeHidden bool) ([]map[string]any, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sourceIDs(items []db.Source) []int64 {
|
||||
out := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.ID > 0 {
|
||||
out = append(out, item.ID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Service) CheckDue(ctx context.Context) {
|
||||
items, err := s.store.ListSources(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
enabled := make([]db.Source, 0, len(items))
|
||||
for _, item := range items {
|
||||
if !item.Enabled {
|
||||
continue
|
||||
}
|
||||
if item.LastCheckedAt != "" {
|
||||
if last, err := time.Parse(time.RFC3339, item.LastCheckedAt); err == nil && now.Sub(last) < time.Duration(item.CheckIntervalSec)*time.Second {
|
||||
continue
|
||||
}
|
||||
}
|
||||
_ = s.CheckOne(ctx, item)
|
||||
enabled = append(enabled, item)
|
||||
}
|
||||
if len(enabled) == 0 {
|
||||
return
|
||||
}
|
||||
const concurrency = 4
|
||||
work := make(chan db.Source)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range work {
|
||||
_ = s.CheckOne(ctx, item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
for _, item := range enabled {
|
||||
work <- item
|
||||
}
|
||||
close(work)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (s *Service) QueueCheckAll() CheckJob {
|
||||
|
||||
@@ -343,21 +343,39 @@ func (r *router) handleBranding(w http.ResponseWriter, req *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "branding": config.SafeBranding(r.effectiveBranding())})
|
||||
case http.MethodPost:
|
||||
var body struct {
|
||||
SiteIconURL string `json:"siteIconUrl"`
|
||||
SiteIconURLSnake string `json:"site_icon_url"`
|
||||
DeveloperAvatarURL string `json:"developerAvatarUrl"`
|
||||
DeveloperAvatarAlt string `json:"developer_avatar_url"`
|
||||
DeveloperName string `json:"developerName"`
|
||||
DeveloperNameSnake string `json:"developer_name"`
|
||||
FeedbackEmail string `json:"feedbackEmail"`
|
||||
FeedbackEmailSnake string `json:"feedback_email"`
|
||||
SiteName string `json:"siteName"`
|
||||
SiteNameSnake string `json:"site_name"`
|
||||
PortalTitle string `json:"portalTitle"`
|
||||
PortalTitleSnake string `json:"portal_title"`
|
||||
PortalSubtitle string `json:"portalSubtitle"`
|
||||
PortalSubtitleSnake string `json:"portal_subtitle"`
|
||||
AdminTitle string `json:"adminTitle"`
|
||||
AdminTitleSnake string `json:"admin_title"`
|
||||
AdminSubtitle string `json:"adminSubtitle"`
|
||||
AdminSubtitleSnake string `json:"admin_subtitle"`
|
||||
SiteIconURL string `json:"siteIconUrl"`
|
||||
SiteIconURLSnake string `json:"site_icon_url"`
|
||||
LogoURL string `json:"logoUrl"`
|
||||
LogoURLSnake string `json:"logo_url"`
|
||||
DeveloperAvatarURL string `json:"developerAvatarUrl"`
|
||||
DeveloperAvatarAlt string `json:"developer_avatar_url"`
|
||||
DeveloperName string `json:"developerName"`
|
||||
DeveloperNameSnake string `json:"developer_name"`
|
||||
FeedbackEmail string `json:"feedbackEmail"`
|
||||
FeedbackEmailSnake string `json:"feedback_email"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err)
|
||||
return
|
||||
}
|
||||
next := config.BrandingConfig{
|
||||
SiteName: firstNonEmpty(body.SiteName, body.SiteNameSnake),
|
||||
PortalTitle: firstNonEmpty(body.PortalTitle, body.PortalTitleSnake),
|
||||
PortalSubtitle: firstNonEmpty(body.PortalSubtitle, body.PortalSubtitleSnake),
|
||||
AdminTitle: firstNonEmpty(body.AdminTitle, body.AdminTitleSnake),
|
||||
AdminSubtitle: firstNonEmpty(body.AdminSubtitle, body.AdminSubtitleSnake),
|
||||
SiteIconURL: firstNonEmpty(body.SiteIconURL, body.SiteIconURLSnake),
|
||||
LogoURL: firstNonEmpty(body.LogoURL, body.LogoURLSnake),
|
||||
DeveloperAvatarURL: firstNonEmpty(body.DeveloperAvatarURL, body.DeveloperAvatarAlt),
|
||||
DeveloperName: firstNonEmpty(body.DeveloperName, body.DeveloperNameSnake),
|
||||
FeedbackEmail: firstNonEmpty(body.FeedbackEmail, body.FeedbackEmailSnake),
|
||||
|
||||
@@ -138,6 +138,9 @@ func TestClientBootstrapAndEndpointsShape(t *testing.T) {
|
||||
if branding["developerName"] != "YMhut" || branding["feedbackEmail"] != "support@ymhut.cn" {
|
||||
t.Fatalf("unexpected branding defaults: %#v", branding)
|
||||
}
|
||||
if branding["siteName"] != "YMhut Box" || branding["portalTitle"] != "YMhut Box 统一服务门户" || branding["adminTitle"] != "YMhut 统一管理台" {
|
||||
t.Fatalf("unexpected extended branding defaults: %#v", branding)
|
||||
}
|
||||
}
|
||||
if payload["ok"] != true {
|
||||
t.Fatalf("%s missing ok=true: %#v", path, payload)
|
||||
@@ -194,7 +197,7 @@ func TestAdminAuditPaginationAndBranding(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 0; i < 40; i++ {
|
||||
body := strings.NewReader(`{"developerName":"YMhut","feedbackEmail":"support@ymhut.cn"}`)
|
||||
body := strings.NewReader(`{"siteName":"YMhut Box Pro","portalTitle":"前台服务门户","portalSubtitle":"公开发布与接口状态","adminTitle":"后台统一服务管理","adminSubtitle":"发布、反馈与运维","siteIconUrl":"/assets/favicon.ico","logoUrl":"/assets/favicon.ico","developerName":"YMhut","feedbackEmail":"support@ymhut.cn"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/system/branding", body)
|
||||
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
|
||||
req.Header.Set("X-CSRF-Token", csrf)
|
||||
@@ -204,6 +207,17 @@ func TestAdminAuditPaginationAndBranding(t *testing.T) {
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("branding save %d returned %d: %s", i, res.Code, res.Body.String())
|
||||
}
|
||||
if i == 0 {
|
||||
var saved struct {
|
||||
Branding map[string]any `json:"branding"`
|
||||
}
|
||||
if err := json.Unmarshal(res.Body.Bytes(), &saved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.Branding["siteName"] != "YMhut Box Pro" || saved.Branding["portalTitle"] != "前台服务门户" || saved.Branding["adminTitle"] != "后台统一服务管理" || saved.Branding["logoUrl"] != "/assets/favicon.ico" {
|
||||
t.Fatalf("extended branding fields not saved: %#v", saved.Branding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/system/audit?page=1&perPage=35&type=system.branding.saved", nil)
|
||||
|
||||
Reference in New Issue
Block a user