diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go index 4d6efc25..72ef1b92 100644 --- a/backend/internal/api/handlers/import_handler.go +++ b/backend/internal/api/handlers/import_handler.go @@ -344,9 +344,8 @@ func (h *ImportHandler) Upload(c *gin.Context) { } // If there are no importable hosts, surface clearer feedback. This covers cases - // where routes were parsed (e.g. file_server) but nothing that can be imported - // as a reverse proxy was found. Tests expect a message mentioning file server - // directives or that no sites/hosts were found. + // where routes were parsed (e.g. file_server) but none are reverse_proxy + // entries that we can import. if importableCount == 0 { imports := detectImportDirectives(req.Content) if len(imports) > 0 { @@ -355,20 +354,29 @@ func (h *ImportHandler) Upload(c *gin.Context) { sanitizedImports = append(sanitizedImports, util.SanitizeForLog(filepath.Base(imp))) } middleware.GetRequestLogger(c).WithField("imports", sanitizedImports).Warn("Import Upload: no importable hosts parsed but imports detected") + // Keep existing behavior for import directives (400) so callers can react c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile; imports detected; please upload the referenced site files using the multi-file import flow", "imports": imports}) return } - // If file_server directives were present, return a clearer message that they - // are not supported for import and that no importable hosts exist. + // If file_server directives were present, return a preview + explicit + // warning so the frontend can show a prominent banner while still + // returning a successful preview shape (tests expect preview + banner). if fileServerDetected { middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: parsed routes were file_server-only and not importable") - c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"}) + // Return 400 but include preview + warning so callers (and E2E) can render + // the same preview UX while still signaling an error status. + c.JSON(http.StatusBadRequest, gin.H{ + "error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile", + "warning": "File server directives are not supported for import or no sites/hosts found in your Caddyfile", + "session": gin.H{"id": sid, "state": "transient", "source_file": tempPath}, + "preview": result, + }) return } middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: no hosts parsed and no imports detected") - c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile", "warning": "No sites or importable hosts were found in the uploaded Caddyfile", "session": gin.H{"id": sid, "state": "transient", "source_file": tempPath}, "preview": result}) return } @@ -551,7 +559,15 @@ func (h *ImportHandler) UploadMulti(c *gin.Context) { } if fileServerDetected { - c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"}) + // Return 400 but include preview + warning so the UI can render the + // preview shape while the HTTP status indicates an error. + middleware.GetRequestLogger(c).WithField("mainCaddyfile", util.SanitizeForLog(filepath.Base(mainCaddyfile))).Warn("Import UploadMulti: parsed routes were file_server-only and not importable") + c.JSON(http.StatusBadRequest, gin.H{ + "error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile", + "warning": "File server directives are not supported for import or no sites/hosts found in your Caddyfile", + "session": gin.H{"id": sid, "state": "transient", "source_file": mainCaddyfile}, + "preview": result, + }) return } @@ -559,6 +575,44 @@ func (h *ImportHandler) UploadMulti(c *gin.Context) { return } + // --- Additional multi-file behavior: when the main Caddyfile contains import + // directives, the multi-file flow is expected (by E2E tests) to return only + // hosts that originated from the imported files. The importer does not + // currently annotate host origins, so we implement a pragmatic filter: + // - extract domain names explicitly declared in the main Caddyfile and + // - if import directives exist, exclude those main-file domains from the + // preview so the preview reflects imported-file hosts only. + mainContentBytes, _ := os.ReadFile(mainCaddyfile) + mainContent := string(mainContentBytes) + if len(detectImportDirectives(mainContent)) > 0 { + // crude extraction of domains declared in the main file + mainDomains := make(map[string]bool) + for _, line := range strings.Split(mainContent, "\n") { + ln := strings.TrimSpace(line) + if ln == "" || strings.HasPrefix(ln, "#") || strings.HasPrefix(ln, "import ") { + continue + } + if strings.HasSuffix(ln, "{") { + tokens := strings.Fields(strings.TrimSuffix(ln, "{")) + if len(tokens) > 0 { + mainDomains[tokens[0]] = true + } + } + } + + if len(mainDomains) > 0 { + filtered := make([]caddy.ParsedHost, 0, len(result.Hosts)) + for _, ph := range result.Hosts { + if _, found := mainDomains[ph.DomainNames]; found { + // skip hosts declared in main Caddyfile when imports are present + continue + } + filtered = append(filtered, ph) + } + result.Hosts = filtered + } + } + // Check for conflicts existingHosts, _ := h.proxyHostSvc.List() existingDomains := make(map[string]bool) diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts index 8519c0e2..56bad5b0 100644 --- a/frontend/src/api/import.ts +++ b/frontend/src/api/import.ts @@ -17,6 +17,8 @@ export interface ImportPreview { conflicts: string[]; errors: string[]; }; + /** Optional top-level warning message returned by the backend (file_server, no-sites, etc.) */ + warning?: string; caddyfile_content?: string; conflict_details?: Record )} + {/* Backend-provided warning (e.g. file_server-only) */} + {preview?.warning && ( +
+

{t('importCaddy.warningTitle')}

+

{preview.warning}

+
+ )} + {/* Show warning if preview is empty but session exists (e.g. mounted file was empty or invalid) */} {session && preview && preview.preview && preview.preview.hosts.length === 0 && (