diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 2b2aa84..7b4d25c 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -106,7 +106,14 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string NotifyError: func(err error) { logger.Error("发送邮件通知失败", "error", err) }, CurrentCertificate: certificates.Current, } - renewal := certificate.NewRenewalScheduler(repo, workflow, cfg.ACME.RenewalThresholdDays) + renewal := certificate.NewRenewalSchedulerWithPolicy(repo, workflow, certificate.RenewalPolicy{ + CheckInterval: time.Duration(cfg.ACME.RenewalCheckIntervalDays) * 24 * time.Hour, + NearExpiryThreshold: time.Duration(cfg.ACME.RenewalNearExpiryDays) * 24 * time.Hour, + RenewalThreshold: time.Duration(cfg.ACME.RenewalThresholdDays) * 24 * time.Hour, + UrgentThreshold: time.Duration(cfg.ACME.RenewalUrgentThresholdDays) * 24 * time.Hour, + MaxRetries: cfg.ACME.RenewalMaxRetries, + RetryInterval: time.Duration(cfg.ACME.RenewalRetryIntervalMins) * time.Minute, + }) server := httpapi.New(httpapi.Dependencies{ Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys, SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(), diff --git a/backend/internal/certificate/renewal.go b/backend/internal/certificate/renewal.go index 6ad8523..75671fd 100644 --- a/backend/internal/certificate/renewal.go +++ b/backend/internal/certificate/renewal.go @@ -18,33 +18,52 @@ type CertificateIssuer interface { Issue(ctx context.Context, domainID string) (IssueResult, error) } +type RenewalPolicy struct { + CheckInterval time.Duration + NearExpiryThreshold time.Duration + RenewalThreshold time.Duration + UrgentThreshold time.Duration + MaxRetries int + RetryInterval time.Duration +} + type RenewalScheduler struct { - lister DomainLister - issuer CertificateIssuer - threshold time.Duration - now func() time.Time - interval time.Duration - mu sync.Mutex - runMu sync.Mutex - running bool + lister DomainLister + issuer CertificateIssuer + policy RenewalPolicy + now func() time.Time + mu sync.Mutex + runMu sync.Mutex + running bool } type RenewalReport struct { - Checked int `json:"checked"` - Selected int `json:"selected"` - Succeeded int `json:"succeeded"` - Failed int `json:"failed"` - Errors []string `json:"errors,omitempty"` + Checked int `json:"checked"` + NearExpiry int `json:"near_expiry"` + Selected int `json:"selected"` + Urgent int `json:"urgent"` + Succeeded int `json:"succeeded"` + Failed int `json:"failed"` + Errors []string `json:"errors,omitempty"` } func NewRenewalScheduler(lister DomainLister, issuer CertificateIssuer, thresholdDays int) *RenewalScheduler { if thresholdDays < 1 { thresholdDays = 6 } - return &RenewalScheduler{ - lister: lister, issuer: issuer, threshold: time.Duration(thresholdDays) * 24 * time.Hour, - now: time.Now, interval: 24 * time.Hour, - } + return NewRenewalSchedulerWithPolicy(lister, issuer, RenewalPolicy{ + CheckInterval: 3 * 24 * time.Hour, + NearExpiryThreshold: 10 * 24 * time.Hour, + RenewalThreshold: time.Duration(thresholdDays) * 24 * time.Hour, + UrgentThreshold: 3 * 24 * time.Hour, + MaxRetries: 3, + RetryInterval: 30 * time.Minute, + }) +} + +func NewRenewalSchedulerWithPolicy(lister DomainLister, issuer CertificateIssuer, policy RenewalPolicy) *RenewalScheduler { + policy = normalizeRenewalPolicy(policy) + return &RenewalScheduler{lister: lister, issuer: issuer, policy: policy, now: time.Now} } func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) { @@ -65,11 +84,19 @@ func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) { if !item.AutoRenew || item.Status == "issuing" { continue } - if item.ExpiresAt != nil && item.ExpiresAt.Sub(now) > s.threshold { + remaining := renewalRemaining(item.ExpiresAt, now) + if item.ExpiresAt != nil && remaining <= s.policy.NearExpiryThreshold { + report.NearExpiry++ + } + if item.ExpiresAt != nil && remaining > s.policy.RenewalThreshold { continue } report.Selected++ - if _, err := s.issuer.Issue(ctx, item.ID); err != nil { + urgent := item.ExpiresAt != nil && remaining <= s.policy.UrgentThreshold + if urgent { + report.Urgent++ + } + if err := s.issueWithRetry(ctx, item.ID, urgent); err != nil { report.Failed++ report.Errors = append(report.Errors, fmt.Sprintf("%s:%v", item.Name, err)) continue @@ -96,16 +123,84 @@ func (s *RenewalScheduler) Start(ctx context.Context) { s.running = false s.mu.Unlock() }() - _, _ = s.RunOnce(ctx) - ticker := time.NewTicker(s.interval) - defer ticker.Stop() + report, err := s.RunOnce(ctx) for { + interval := s.policy.CheckInterval + if err == nil && report.NearExpiry > 0 { + interval = 24 * time.Hour + } + timer := time.NewTimer(interval) select { case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } return - case <-ticker.C: - _, _ = s.RunOnce(ctx) + case <-timer.C: } + report, err = s.RunOnce(ctx) } }() } + +func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string, urgent bool) error { + attempts := 1 + if urgent { + attempts = s.policy.MaxRetries + } + var lastErr error + for attempt := 1; attempt <= attempts; attempt++ { + if _, err := s.issuer.Issue(ctx, domainID); err == nil { + return nil + } else { + lastErr = err + } + if attempt == attempts { + break + } + if err := waitForRenewalRetry(ctx, s.policy.RetryInterval); err != nil { + return fmt.Errorf("第 %d 次续期失败,等待重试被取消:%w", attempt, err) + } + } + return fmt.Errorf("续期失败(已尝试 %d 次):%w", attempts, lastErr) +} + +func waitForRenewalRetry(ctx context.Context, interval time.Duration) error { + timer := time.NewTimer(interval) + defer timer.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + return nil + } +} + +func renewalRemaining(expiresAt *time.Time, now time.Time) time.Duration { + if expiresAt == nil { + return 0 + } + return expiresAt.Sub(now) +} + +func normalizeRenewalPolicy(policy RenewalPolicy) RenewalPolicy { + if policy.CheckInterval <= 0 { + policy.CheckInterval = 3 * 24 * time.Hour + } + if policy.NearExpiryThreshold <= 0 { + policy.NearExpiryThreshold = 10 * 24 * time.Hour + } + if policy.RenewalThreshold <= 0 { + policy.RenewalThreshold = 6 * 24 * time.Hour + } + if policy.UrgentThreshold <= 0 { + policy.UrgentThreshold = 3 * 24 * time.Hour + } + if policy.MaxRetries <= 0 { + policy.MaxRetries = 3 + } + if policy.RetryInterval <= 0 { + policy.RetryInterval = 30 * time.Minute + } + return policy +} diff --git a/backend/internal/certificate/renewal_test.go b/backend/internal/certificate/renewal_test.go index 280090f..f22ed16 100644 --- a/backend/internal/certificate/renewal_test.go +++ b/backend/internal/certificate/renewal_test.go @@ -53,7 +53,7 @@ func TestRenewalSchedulerSelectsOnlyExpiringAutoRenewDomains(t *testing.T) { func TestRenewalSchedulerReportsFailures(t *testing.T) { now := time.Now().UTC() - expires := now.Add(time.Hour) + expires := now.Add(4 * 24 * time.Hour) item := domain.Domain{ID: "one", Name: "one.example.com", AutoRenew: true, ExpiresAt: &expires} scheduler := NewRenewalScheduler(renewalLister{items: []domain.Domain{item}}, &renewalService{err: errors.New("模拟续期失败")}, 6) report, err := scheduler.RunOnce(context.Background()) @@ -64,3 +64,54 @@ func TestRenewalSchedulerReportsFailures(t *testing.T) { t.Fatalf("续期失败结果错误:%+v", report) } } + +func TestRenewalSchedulerRetriesUrgentRenewal(t *testing.T) { + now := time.Date(2026, 8, 26, 0, 0, 0, 0, time.UTC) + expires := now.Add(2 * 24 * time.Hour) + service := &renewalService{err: errors.New("模拟紧急续期失败")} + scheduler := NewRenewalSchedulerWithPolicy(renewalLister{items: []domain.Domain{{ + ID: "urgent", Name: "urgent.example.com", AutoRenew: true, ExpiresAt: &expires, + }}}, service, RenewalPolicy{ + CheckInterval: time.Hour, + NearExpiryThreshold: 10 * 24 * time.Hour, + RenewalThreshold: 6 * 24 * time.Hour, + UrgentThreshold: 3 * 24 * time.Hour, + MaxRetries: 3, + RetryInterval: time.Nanosecond, + }) + scheduler.now = func() time.Time { return now } + + report, err := scheduler.RunOnce(context.Background()) + if err != nil { + t.Fatal(err) + } + if report.Selected != 1 || report.Urgent != 1 || report.Failed != 1 || report.Succeeded != 0 { + t.Fatalf("紧急续期报告错误:%+v", report) + } + if len(service.ids) != 3 { + t.Fatalf("紧急续期应按最大重试次数执行:%v", service.ids) + } +} + +func TestRenewalSchedulerStopsRetryWhenContextCanceled(t *testing.T) { + now := time.Now().UTC() + expires := now.Add(2 * 24 * time.Hour) + service := &renewalService{err: errors.New("模拟紧急续期失败")} + scheduler := NewRenewalSchedulerWithPolicy(renewalLister{items: []domain.Domain{{ + ID: "urgent", Name: "urgent.example.com", AutoRenew: true, ExpiresAt: &expires, + }}}, service, RenewalPolicy{MaxRetries: 3, RetryInterval: time.Hour}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + time.Sleep(5 * time.Millisecond) + cancel() + }() + report, err := scheduler.RunOnce(ctx) + if err != nil { + t.Fatal(err) + } + if report.Failed != 1 || len(service.ids) != 1 { + t.Fatalf("取消后的紧急续期重试报告错误:%+v calls=%d", report, len(service.ids)) + } +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 2bbf878..be8ffe6 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -77,7 +77,12 @@ type ACMEConfig struct { CA string `json:"ca"` Environment string `json:"environment"` AccountEmail string `json:"account_email"` + RenewalCheckIntervalDays int `json:"renewal_check_interval_days"` + RenewalNearExpiryDays int `json:"renewal_near_expiry_days"` RenewalThresholdDays int `json:"renewal_threshold_days"` + RenewalUrgentThresholdDays int `json:"renewal_urgent_threshold_days"` + RenewalMaxRetries int `json:"renewal_max_retries"` + RenewalRetryIntervalMins int `json:"renewal_retry_interval_mins"` PropagationTimeoutSecs int `json:"propagation_timeout_secs"` PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"` } @@ -132,7 +137,12 @@ func Default(baseDir string) Config { ACME: ACMEConfig{ CA: "letsencrypt", Environment: "staging", + RenewalCheckIntervalDays: 3, + RenewalNearExpiryDays: 10, RenewalThresholdDays: 6, + RenewalUrgentThresholdDays: 3, + RenewalMaxRetries: 3, + RenewalRetryIntervalMins: 30, PropagationTimeoutSecs: 900, PropagationPollingIntervalSecs: 10, }, @@ -258,7 +268,12 @@ func (c *Config) ApplyEnvironment() error { target *int }{ {"SCERT_SERVER_PORT", &c.Server.Port}, + {"SCERT_ACME_RENEWAL_CHECK_INTERVAL_DAYS", &c.ACME.RenewalCheckIntervalDays}, + {"SCERT_ACME_RENEWAL_NEAR_EXPIRY_DAYS", &c.ACME.RenewalNearExpiryDays}, {"SCERT_ACME_RENEWAL_THRESHOLD_DAYS", &c.ACME.RenewalThresholdDays}, + {"SCERT_ACME_RENEWAL_URGENT_THRESHOLD_DAYS", &c.ACME.RenewalUrgentThresholdDays}, + {"SCERT_ACME_RENEWAL_MAX_RETRIES", &c.ACME.RenewalMaxRetries}, + {"SCERT_ACME_RENEWAL_RETRY_INTERVAL_MINS", &c.ACME.RenewalRetryIntervalMins}, {"SCERT_ACME_PROPAGATION_TIMEOUT_SECS", &c.ACME.PropagationTimeoutSecs}, {"SCERT_ACME_PROPAGATION_POLLING_INTERVAL_SECS", &c.ACME.PropagationPollingIntervalSecs}, } @@ -295,6 +310,24 @@ func (c *Config) Normalize() { if c.ACME.PropagationTimeoutSecs == 0 { c.ACME.PropagationTimeoutSecs = 900 } + if c.ACME.RenewalCheckIntervalDays == 0 { + c.ACME.RenewalCheckIntervalDays = 3 + } + if c.ACME.RenewalNearExpiryDays == 0 { + c.ACME.RenewalNearExpiryDays = 10 + } + if c.ACME.RenewalThresholdDays == 0 { + c.ACME.RenewalThresholdDays = 6 + } + if c.ACME.RenewalUrgentThresholdDays == 0 { + c.ACME.RenewalUrgentThresholdDays = 3 + } + if c.ACME.RenewalMaxRetries == 0 { + c.ACME.RenewalMaxRetries = 3 + } + if c.ACME.RenewalRetryIntervalMins == 0 { + c.ACME.RenewalRetryIntervalMins = 30 + } if c.ACME.PropagationPollingIntervalSecs == 0 { c.ACME.PropagationPollingIntervalSecs = 10 } @@ -435,6 +468,21 @@ func (c Config) Validate() error { if c.ACME.RenewalThresholdDays < 1 { return fmt.Errorf("续期阈值必须大于 0") } + if c.ACME.RenewalCheckIntervalDays < 1 || c.ACME.RenewalNearExpiryDays < 1 || c.ACME.RenewalUrgentThresholdDays < 1 { + return fmt.Errorf("续期检查阈值必须大于 0") + } + if c.ACME.RenewalUrgentThresholdDays > c.ACME.RenewalThresholdDays { + return fmt.Errorf("紧急续期阈值不能大于自动续期阈值") + } + if c.ACME.RenewalThresholdDays > c.ACME.RenewalNearExpiryDays { + return fmt.Errorf("自动续期阈值不能大于临近到期检查阈值") + } + if c.ACME.RenewalMaxRetries < 1 || c.ACME.RenewalMaxRetries > 10 { + return fmt.Errorf("续期最大重试次数必须在 1 到 10 次之间") + } + if c.ACME.RenewalRetryIntervalMins < 1 || c.ACME.RenewalRetryIntervalMins > 1440 { + return fmt.Errorf("续期重试间隔必须在 1 到 1440 分钟之间") + } if c.ACME.PropagationTimeoutSecs < 1 || c.ACME.PropagationTimeoutSecs > 86400 { return fmt.Errorf("DNS 传播等待时间必须在 1 到 86400 秒之间") } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 8307cbe..6841d6c 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -161,6 +161,52 @@ func TestDefaultACMEPropagationSettings(t *testing.T) { } } +func TestDefaultRenewalPolicy(t *testing.T) { + cfg := Default(t.TempDir()) + if cfg.ACME.RenewalCheckIntervalDays != 3 || cfg.ACME.RenewalNearExpiryDays != 10 || + cfg.ACME.RenewalThresholdDays != 6 || cfg.ACME.RenewalUrgentThresholdDays != 3 || + cfg.ACME.RenewalMaxRetries != 3 || cfg.ACME.RenewalRetryIntervalMins != 30 { + t.Fatalf("默认续期策略不正确:%+v", cfg.ACME) + } +} + +func TestNormalizeRestoresMissingRenewalPolicy(t *testing.T) { + cfg := Default(t.TempDir()) + cfg.ACME.RenewalCheckIntervalDays = 0 + cfg.ACME.RenewalNearExpiryDays = 0 + cfg.ACME.RenewalThresholdDays = 0 + cfg.ACME.RenewalUrgentThresholdDays = 0 + cfg.ACME.RenewalMaxRetries = 0 + cfg.ACME.RenewalRetryIntervalMins = 0 + cfg.Normalize() + if cfg.ACME.RenewalCheckIntervalDays != 3 || cfg.ACME.RenewalNearExpiryDays != 10 || + cfg.ACME.RenewalThresholdDays != 6 || cfg.ACME.RenewalUrgentThresholdDays != 3 || + cfg.ACME.RenewalMaxRetries != 3 || cfg.ACME.RenewalRetryIntervalMins != 30 { + t.Fatalf("旧配置的续期策略未补齐:%+v", cfg.ACME) + } +} + +func TestValidateRejectsInvalidRenewalPolicy(t *testing.T) { + cases := []struct { + name string + edit func(*Config) + }{ + {name: "urgent threshold", edit: func(cfg *Config) { cfg.ACME.RenewalUrgentThresholdDays = 7 }}, + {name: "renewal threshold", edit: func(cfg *Config) { cfg.ACME.RenewalThresholdDays = 11 }}, + {name: "retry count", edit: func(cfg *Config) { cfg.ACME.RenewalMaxRetries = 0 }}, + {name: "retry interval", edit: func(cfg *Config) { cfg.ACME.RenewalRetryIntervalMins = 0 }}, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + cfg := Default(t.TempDir()) + item.edit(&cfg) + if err := cfg.Validate(); err == nil { + t.Fatal("无效续期策略未被拒绝") + } + }) + } +} + func TestNormalizeRestoresMissingACMEPropagationSettings(t *testing.T) { cfg := Default(t.TempDir()) cfg.ACME.PropagationTimeoutSecs = 0 diff --git a/backend/internal/httpapi/settings_api.go b/backend/internal/httpapi/settings_api.go index 8440a06..5edeb03 100644 --- a/backend/internal/httpapi/settings_api.go +++ b/backend/internal/httpapi/settings_api.go @@ -40,7 +40,12 @@ type settingsACMEView struct { CA string `json:"ca"` Environment string `json:"environment"` AccountEmail string `json:"account_email"` + RenewalCheckIntervalDays int `json:"renewal_check_interval_days"` + RenewalNearExpiryDays int `json:"renewal_near_expiry_days"` RenewalThresholdDays int `json:"renewal_threshold_days"` + RenewalUrgentThresholdDays int `json:"renewal_urgent_threshold_days"` + RenewalMaxRetries int `json:"renewal_max_retries"` + RenewalRetryIntervalMins int `json:"renewal_retry_interval_mins"` PropagationTimeoutSecs int `json:"propagation_timeout_secs"` PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"` } @@ -106,7 +111,12 @@ type settingsACMERequest struct { CA *string `json:"ca"` Environment *string `json:"environment"` AccountEmail *string `json:"account_email"` + RenewalCheckIntervalDays *int `json:"renewal_check_interval_days"` + RenewalNearExpiryDays *int `json:"renewal_near_expiry_days"` RenewalThresholdDays *int `json:"renewal_threshold_days"` + RenewalUrgentThresholdDays *int `json:"renewal_urgent_threshold_days"` + RenewalMaxRetries *int `json:"renewal_max_retries"` + RenewalRetryIntervalMins *int `json:"renewal_retry_interval_mins"` PropagationTimeoutSecs *int `json:"propagation_timeout_secs"` PropagationPollingIntervalSecs *int `json:"propagation_polling_interval_secs"` } @@ -233,8 +243,14 @@ func settingsViewFromConfig(cfg config.Config) settingsView { Server: settingsServerView{Host: cfg.Server.Host, Port: cfg.Server.Port, HTTPSMode: cfg.Server.HTTPSMode}, Web: settingsWebView{Enabled: cfg.Web.Enabled, Hostname: cfg.Web.Hostname, PublicIP: cfg.Web.PublicIP}, Paths: settingsPathsView{Certificate: cfg.Paths.Certificate}, - ACME: settingsACMEView{CA: cfg.ACME.CA, Environment: cfg.ACME.Environment, AccountEmail: cfg.ACME.AccountEmail, RenewalThresholdDays: cfg.ACME.RenewalThresholdDays, PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs}, - DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState}, + ACME: settingsACMEView{ + CA: cfg.ACME.CA, Environment: cfg.ACME.Environment, AccountEmail: cfg.ACME.AccountEmail, + RenewalCheckIntervalDays: cfg.ACME.RenewalCheckIntervalDays, RenewalNearExpiryDays: cfg.ACME.RenewalNearExpiryDays, + RenewalThresholdDays: cfg.ACME.RenewalThresholdDays, RenewalUrgentThresholdDays: cfg.ACME.RenewalUrgentThresholdDays, + RenewalMaxRetries: cfg.ACME.RenewalMaxRetries, RenewalRetryIntervalMins: cfg.ACME.RenewalRetryIntervalMins, + PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs, + }, + DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState}, Notification: settingsNotificationView{ HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider, SMTPHost: cfg.Notification.SMTPHost, SMTPPort: cfg.Notification.SMTPPort, SMTPUsername: cfg.Notification.SMTPUsername, @@ -293,6 +309,21 @@ func applySettingsRequest(cfg *config.Config, request settingsRequest) { if request.ACME.RenewalThresholdDays != nil { cfg.ACME.RenewalThresholdDays = *request.ACME.RenewalThresholdDays } + if request.ACME.RenewalCheckIntervalDays != nil { + cfg.ACME.RenewalCheckIntervalDays = *request.ACME.RenewalCheckIntervalDays + } + if request.ACME.RenewalNearExpiryDays != nil { + cfg.ACME.RenewalNearExpiryDays = *request.ACME.RenewalNearExpiryDays + } + if request.ACME.RenewalUrgentThresholdDays != nil { + cfg.ACME.RenewalUrgentThresholdDays = *request.ACME.RenewalUrgentThresholdDays + } + if request.ACME.RenewalMaxRetries != nil { + cfg.ACME.RenewalMaxRetries = *request.ACME.RenewalMaxRetries + } + if request.ACME.RenewalRetryIntervalMins != nil { + cfg.ACME.RenewalRetryIntervalMins = *request.ACME.RenewalRetryIntervalMins + } if request.ACME.PropagationTimeoutSecs != nil { cfg.ACME.PropagationTimeoutSecs = *request.ACME.PropagationTimeoutSecs } diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index 09d5ec0..e9f048e 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -224,7 +224,12 @@ export function useDashboard() { ca: "letsencrypt", environment: "staging", account_email: "", + renewal_check_interval_days: 3, + renewal_near_expiry_days: 10, renewal_threshold_days: 6, + renewal_urgent_threshold_days: 3, + renewal_max_retries: 3, + renewal_retry_interval_mins: 30, propagation_timeout_secs: 900, propagation_polling_interval_secs: 10, }, diff --git a/frontend/src/views/Dashboard/Dashboard.vue b/frontend/src/views/Dashboard/Dashboard.vue index 3a4a0f2..ccd3252 100644 --- a/frontend/src/views/Dashboard/Dashboard.vue +++ b/frontend/src/views/Dashboard/Dashboard.vue @@ -423,6 +423,18 @@ const { +