diff --git a/api/caddy_manager.py b/api/caddy_manager.py index dcb6154..3971feb 100644 --- a/api/caddy_manager.py +++ b/api/caddy_manager.py @@ -649,11 +649,12 @@ class CaddyManager(BaseServiceManager): # ── Active cert management ──────────────────────────────────────────── def renew_cert(self) -> Dict[str, Any]: - """Trigger ACME cert renewal by reloading Caddy. + """Regenerate the Caddyfile, reload Caddy, and trigger ACME cert renewal. - Returns immediately with status='pending' so the caller can poll - GET /api/caddy/cert-status to track progress. Not applicable to - LAN mode — callers should use upload_custom_cert() instead. + Regenerates first so a stale or broken on-disk Caddyfile never blocks + the reload. Returns immediately with status='pending'; the caller + polls GET /api/caddy/cert-status to track progress. Not applicable + to LAN mode — callers should use upload_custom_cert() instead. """ ident = (self.config_manager.get_identity() if self.config_manager else {}) or {} domain_mode = ident.get('domain_mode', 'lan') @@ -665,7 +666,17 @@ class CaddyManager(BaseServiceManager): 'Upload a custom certificate instead.', } - if not self.reload_caddy(): + # Regenerate → write → reload in one shot so the Caddyfile is always fresh. + if self.config_manager: + try: + ok = self.regenerate_with_installed([]) + except Exception as exc: + logger.error('renew_cert: regenerate_with_installed failed: %s', exc) + ok = False + else: + ok = self.reload_caddy() + + if not ok: return {'ok': False, 'error': 'Caddy reload failed — check Caddy logs.'} # Invalidate the cached status so the next poll triggers a fresh SSL check. diff --git a/tests/test_caddy_manager.py b/tests/test_caddy_manager.py index 3708d0b..f85590f 100644 --- a/tests/test_caddy_manager.py +++ b/tests/test_caddy_manager.py @@ -493,17 +493,17 @@ class TestRenewCert(unittest.TestCase): self.assertFalse(result['ok']) self.assertIn('LAN', result['error']) - def test_acme_mode_calls_reload(self): + def test_acme_mode_calls_regenerate(self): mgr = _mgr(identity={'domain_mode': 'pic_ngo'}) - with patch.object(mgr, 'reload_caddy', return_value=True) as mock_reload: + with patch.object(mgr, 'regenerate_with_installed', return_value=True) as mock_regen: result = mgr.renew_cert() - mock_reload.assert_called_once() + mock_regen.assert_called_once_with([]) self.assertTrue(result['ok']) self.assertEqual(result['status'], 'pending') def test_reload_failure_propagated(self): mgr = _mgr(identity={'domain_mode': 'cloudflare'}) - with patch.object(mgr, 'reload_caddy', return_value=False): + with patch.object(mgr, 'regenerate_with_installed', return_value=False): result = mgr.renew_cert() self.assertFalse(result['ok']) self.assertIn('reload failed', result['error']) @@ -512,7 +512,7 @@ class TestRenewCert(unittest.TestCase): import time mgr = _mgr(identity={'domain_mode': 'pic_ngo'}) mgr._cert_refreshed_at = time.monotonic() - with patch.object(mgr, 'reload_caddy', return_value=True): + with patch.object(mgr, 'regenerate_with_installed', return_value=True): mgr.renew_cert() self.assertIsNone(mgr._cert_refreshed_at)