From eb60530cec163c2174c7a3111d2741b342695cd7 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 30 Nov 2025 00:32:23 +0000 Subject: [PATCH 001/123] chore: import handler transient error messages --- .../internal/api/handlers/import_handler.go | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go index bbcf4b97..bb58abb0 100644 --- a/backend/internal/api/handlers/import_handler.go +++ b/backend/internal/api/handlers/import_handler.go @@ -278,6 +278,17 @@ func (h *ImportHandler) Upload(c *gin.Context) { return } + // If no hosts were parsed, provide a clearer error when import directives exist + if len(result.Hosts) == 0 { + imports := detectImportDirectives(req.Content) + if len(imports) > 0 { + 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 + } + c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile"}) + return + } + // Check for conflicts with existing hosts and build conflict details existingHosts, _ := h.proxyHostSvc.List() existingDomainsMap := make(map[string]models.ProxyHost) @@ -415,6 +426,19 @@ func (h *ImportHandler) UploadMulti(c *gin.Context) { return } + // If parsing succeeded but no hosts were found, and imports were present in the main file, + // inform the caller to upload the site files. + if len(result.Hosts) == 0 { + mainContentBytes, _ := os.ReadFile(mainCaddyfile) + imports := detectImportDirectives(string(mainContentBytes)) + if len(imports) > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile; import directives detected; please include site files in upload", "imports": imports}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile"}) + return + } + // Check for conflicts existingHosts, _ := h.proxyHostSvc.List() existingDomains := make(map[string]bool) From 2014ff9fce0bcc6b2e842833dea680af9a8050ee Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 30 Nov 2025 00:35:34 +0000 Subject: [PATCH 002/123] feat(import): add multi-site import modal and upload-multi API --- frontend/src/api/import.ts | 5 ++ frontend/src/components/ImportSitesModal.tsx | 90 ++++++++++++++++++++ frontend/src/pages/ImportCaddy.tsx | 14 +++ 3 files changed, 109 insertions(+) create mode 100644 frontend/src/components/ImportSitesModal.tsx diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts index fe7366a6..eb42b0ef 100644 --- a/frontend/src/api/import.ts +++ b/frontend/src/api/import.ts @@ -40,6 +40,11 @@ export const uploadCaddyfile = async (content: string): Promise = return data; }; +export const uploadCaddyfilesMulti = async (contents: string[]): Promise => { + const { data } = await client.post('/import/upload-multi', { contents }); + return data; +}; + export const getImportPreview = async (): Promise => { const { data } = await client.get('/import/preview'); return data; diff --git a/frontend/src/components/ImportSitesModal.tsx b/frontend/src/components/ImportSitesModal.tsx new file mode 100644 index 00000000..79627d0b --- /dev/null +++ b/frontend/src/components/ImportSitesModal.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react' +import { uploadCaddyfilesMulti } from '../api/import' + +type Props = { + visible: boolean + onClose: () => void + onUploaded?: () => void +} + +export default function ImportSitesModal({ visible, onClose, onUploaded }: Props) { + const [sites, setSites] = useState(['']) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + if (!visible) return null + + const setSite = (index: number, value: string) => { + const s = [...sites] + s[index] = value + setSites(s) + } + + const addSite = () => setSites(prev => [...prev, '']) + const removeSite = (index: number) => setSites(prev => prev.filter((_, i) => i !== index)) + + const handleSubmit = async () => { + setError(null) + setLoading(true) + try { + const cleaned = sites.map(s => s || '') + await uploadCaddyfilesMulti(cleaned) + setLoading(false) + onUploaded && onUploaded() + onClose() + } catch (err: any) { + setError(err?.message || 'Upload failed') + setLoading(false) + } + } + + return ( +
+
+
+

Multi-site Import

+

Add each site's Caddyfile content separately, then parse them together.

+ +
+ {sites.map((s, idx) => ( +
+
+
Site {idx + 1}
+
+ {sites.length > 1 && ( + + )} +
+
+