package main import ( "bytes" "fmt" "net/http" "net/url" "strings" "time" ) const ( gatewayManagedKey = "naviGateway" gatewayHostKey = "naviGatewayHost" gatewayAutoIconKey = "naviGatewayAutoIcon" ) func normalizedHost(raw string) string { u, err := url.Parse(raw) if err != nil { return "" } return strings.ToLower(u.Hostname()) } func navURLForRoute(r Route) string { if r.NavURL != "" { return strings.TrimSpace(r.NavURL) } return "https://" + strings.TrimSpace(r.Host) } // validateGatewayNavigation ensures every selected Dashy group exists before either // gateway.yml or conf.yml is changed. func validateGatewayNavigation(gw *GatewayConfig, dashy *Config) error { sections := make(map[string]bool, len(dashy.Sections)) for _, s := range dashy.Sections { sections[strings.TrimSpace(s.Name)] = true } for _, r := range gw.Routes { if !r.NavEnabled { continue } if !sections[strings.TrimSpace(r.NavSection)] { return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection) } } return nil } // syncGatewayNavigation makes gateway.yml the source of truth for proxy-managed // Dashy items. Manually-created items that are unrelated to a managed host remain // untouched. Existing entries for a newly-managed host are adopted instead of // duplicated. func syncGatewayNavigation(gw *GatewayConfig) error { dashy, err := loadConfig() if err != nil { return err } if err := validateGatewayNavigation(gw, dashy); err != nil { return err } managedHosts := make(map[string]bool) for _, r := range gw.Routes { if r.NavEnabled { managedHosts[strings.ToLower(strings.TrimSpace(r.Host))] = true } } // Remove stale proxy-managed items and adopt existing items for hosts that are // now managed by the gateway. The system-owned history group is never touched. for si := range dashy.Sections { if isHistorySection(dashy.Sections[si]) { continue } kept := dashy.Sections[si].Items[:0] for _, item := range dashy.Sections[si].Items { itemHost := normalizedHost(item.URL) wasManaged, _ := item.Extra[gatewayManagedKey].(bool) if wasManaged || managedHosts[itemHost] { continue } kept = append(kept, item) } dashy.Sections[si].Items = kept } for _, r := range gw.Routes { if !r.NavEnabled { continue } idx := -1 for i := range dashy.Sections { if strings.TrimSpace(dashy.Sections[i].Name) == strings.TrimSpace(r.NavSection) { idx = i break } } if idx < 0 { return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection) } icon := strings.TrimSpace(r.NavIcon) if icon == "" { icon = "fas fa-globe" } item := NavItem{ Title: strings.TrimSpace(r.NavName), Description: strings.TrimSpace(r.NavDescription), Icon: icon, URL: navURLForRoute(r), Extra: map[string]any{ gatewayManagedKey: true, gatewayHostKey: strings.ToLower(strings.TrimSpace(r.Host)), gatewayAutoIconKey: r.NavAutoIcon, }, } dashy.Sections[idx].Items = append(dashy.Sections[idx].Items, item) } if err := saveConfig(dashy); err != nil { return err } triggerHistoryRebuild() return nil } func triggerHistoryRebuild() { req, err := http.NewRequest(http.MethodPost, historyBaseURL+"/rebuild", bytes.NewReader(nil)) if err != nil { return } if adminToken != "" { req.Header.Set("Authorization", "Bearer "+adminToken) } client := &http.Client{Timeout: 3 * time.Second} res, err := client.Do(req) if err == nil { _ = res.Body.Close() } }