diff --git a/internal/api/types.go b/internal/api/types.go index fcc2a8a..e3f6575 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -154,4 +154,5 @@ ExpiresAt string `json:"expires_at"` // The expiration date/time of the current certificate. RenewBefore string `json:"renew_before"` // The duration before expiration the renewal is triggered. RotationEnabled bool `json:"rotation_enabled"` // Whether automated rotation is currently enabled. + RemainDays string `json:"remain_days"` // The number of days remaining until expiration. } diff --git a/internal/api_handlers.go b/internal/api_handlers.go index d4f9881..cd0e2cd 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -865,12 +865,27 @@ return } rotatingCerts := make([]*internalapi.RotatingCertificateInfo, 0) + + parser := tool.CertificateParser{} for _, cert := range certs { if cert.EnableRotation { + ci, err := parser.Parse([]byte(cert.CertPEM)) + if err != nil || len(ci) == 0 { + http.Error(w, fmt.Sprintf("failed to parse certificate for domain %s: %v", cert.Domain, err), http.StatusInternalServerError) + return + } + expired_at := ci[0].NotAfter + remaining := time.Until(expired_at) + if remaining < 0 { + remaining = 0 + } + rotatingCerts = append(rotatingCerts, &internalapi.RotatingCertificateInfo{ Domain: cert.Domain, SecretName: cert.SecretName, RenewBefore: cert.RenewBefore.String(), + ExpiresAt: expired_at.Format("2006-01-02"), + RemainDays: fmt.Sprintf("%d%s", int(remaining.Hours()/24), "d"), RotationEnabled: true, }) } diff --git a/internal/pkg/storage/storage.go b/internal/pkg/storage/storage.go index fa37db6..f220712 100644 --- a/internal/pkg/storage/storage.go +++ b/internal/pkg/storage/storage.go @@ -747,6 +747,12 @@ if err = s.deleteMissingResources(ctx, "secrets", secretNames); err != nil { return fmt.Errorf("failed to physically delete missing secrets: %w", err) } + if err = s.deleteCertificateBySecretNames(ctx, secretNames); err != nil { + return fmt.Errorf("failed to physically delete certificates for missing secrets: %w", err) + } + if err = s.deleteMissingResources(ctx, "extension_configs", extensionConfigNames); err != nil { + return fmt.Errorf("failed to physically delete missing extension configs: %w", err) + } if err = s.deleteMissingResources(ctx, "extension_configs", extensionConfigNames); err != nil { return fmt.Errorf("failed to physically delete missing extension configs: %w", err) } @@ -883,3 +889,24 @@ _, err := s.db.ExecContext(ctx, query, args...) return err } + +// deleteCertificateBySecretNames is a helper function to delete a certificate by its secret name. +func (s *Storage) deleteCertificateBySecretNames(ctx context.Context, secretNames []string) error { + if len(secretNames) == 0 { + return nil // No secret names provided, nothing to delete + } + + placeholders := make([]string, len(secretNames)) + args := make([]interface{}, len(secretNames)) + for i, name := range secretNames { + placeholders[i] = s.placeholder(i + 1) + args[i] = name + } + + query := fmt.Sprintf(` + DELETE FROM certificates + WHERE secret_name IN (%s)`, strings.Join(placeholders, ", ")) + + _, err := s.db.ExecContext(ctx, query, args...) + return err +} diff --git a/static/secrets.js b/static/secrets.js index 47b7aa9..c69b4d8 100644 --- a/static/secrets.js +++ b/static/secrets.js @@ -47,6 +47,7 @@ const rotationStatusText = rotationInfo.rotation_enabled ? 'Active' : 'Configured (Disabled)'; const expiresAtText = rotationInfo.expires_at || 'N/A'; + const remainDaysText = rotationInfo.remain_days || 'N/A'; // 2. Assemble the full tooltip content with actual newlines // This content will be used by our CSS tooltip. @@ -55,6 +56,7 @@ Domain: ${rotationInfo.domain} Secret Name: ${rotationInfo.secret_name} Renew Before: ${renewBeforeDisplay} +Remain Days: ${remainDaysText} Expires At: ${expiresAtText} `.trim(); } diff --git a/static/style.css b/static/style.css index 4ad8e5f..b840566 100644 --- a/static/style.css +++ b/static/style.css @@ -1003,7 +1003,7 @@ --tooltip-padding: 8px 12px; --tooltip-font-size: 0.8em; --tooltip-max-width: 300px; - --tooltip-offset: 10px; /* Distance from the element */ + --tooltip-offset: 0px; /* Distance from the element */ position: absolute; visibility: hidden; @@ -1032,7 +1032,7 @@ bottom: calc(100% + var(--tooltip-offset)); /* Position above the element by default */ /* Ensure it doesn't get squished too thin */ - min-width: 150px; + min-width: 180px; } /* Tooltip Arrow */