diff --git a/server/routes/webhook/gitea.js b/server/routes/webhook/gitea.js index 69edde6..da2e44f 100644 --- a/server/routes/webhook/gitea.js +++ b/server/routes/webhook/gitea.js @@ -8,6 +8,7 @@ import { runDeploy, buildPreviewUrl, } from '../../services/previewDeploy.js' +import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js' import { createGitPushWorksheet } from '../../services/gitWorksheet.js' import { ensureProjectTicket } from '../../services/ticketAdmin.js' import { invalidateProjectCache } from '../../services/projectInsights.js' @@ -55,7 +56,19 @@ router.post('/gitea', async (req, res) => { const branch = ref.replace('refs/heads/', '') || 'main' try { - const previewConfigFile = await fetchPreviewConfig(repoFullName, branch) + let previewConfigFile = await fetchPreviewConfig(repoFullName, branch) + // Selbstheilung: Wurde .webklar-preview.json im Repo geloescht (z.B. beim + // Leerraeumen/Ueberarbeiten der Seite), aber das Projekt ist in Appwrite als + // gehostet bekannt, dann Config aus dem gespeicherten Stand wiederherstellen + // (Datei ins Repo zurueckschreiben) und normal weiter-deployen. + if (!previewConfigFile) { + try { + const restored = await ensureRepoPreviewConfig(repoFullName, branch) + if (restored) previewConfigFile = restored + } catch (err) { + console.error('[webhook] preview-config-Wiederherstellung fehlgeschlagen:', err.message) + } + } const previewConfig = previewConfigFile || defaultPreviewConfig(repoFullName) const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile) // Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen - diff --git a/server/services/giteaAdmin.js b/server/services/giteaAdmin.js index 986d7e3..ced5d1a 100644 --- a/server/services/giteaAdmin.js +++ b/server/services/giteaAdmin.js @@ -231,6 +231,47 @@ async function updatePreviewConfigFile(repoFullName, previewConfig, branch = 'ma }) } +/** + * Selbstheilung fuer eine geloeschte/fehlende .webklar-preview.json: + * Rekonstruiert die Hosting-Config aus dem in Appwrite gespeicherten + * Projektstand (Subdomain, Typ, Name), schreibt sie ins Repo zurueck und liefert + * sie fuer einen sofortigen Deploy zurueck. Gibt null zurueck, wenn zu dem Repo + * kein gehostetes, aktives Projekt bekannt ist (dann bleibt es Repo-only). + */ +export async function ensureRepoPreviewConfig(repoFullName, branch = 'main') { + if (!repoFullName) return null + const docs = await listDocuments(config.collections.websiteProjects, [ + Query.equal('repoFullName', repoFullName), + Query.limit(1), + ]) + const doc = docs[0] + if (!doc || doc.archived) return null + + const type = deriveProjectType(doc) + const hasWebsite = type === 'preview' || type === 'website' || type === 'template' + const subdomain = doc.subdomain || '' + if (!hasWebsite || !subdomain) return null + + const previewConfig = { + enabled: true, + type: 'static', + branch, + displayName: doc.projectName || repoFullName.split('/').pop(), + templateName: doc.templateName || config.gitea.templateRepo, + projectType: type, + public: type === 'website', + template: type === 'template', + subdomain, + } + + try { + await updatePreviewConfigFile(repoFullName, previewConfig, branch) + } catch (err) { + console.error('[ensureRepoPreviewConfig] .webklar-preview.json zurueckschreiben fehlgeschlagen:', err.message) + } + return previewConfig +} + export async function createProjectFromTemplate({ repoName, displayName, diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index 7f9b273..417527b 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -70,10 +70,74 @@ async function cloneRepo(cloneUrl, targetDir, branch) { } async function deployStatic(sourceDir, targetDir, indexSubdir = '') { - const webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir + let webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir + // Falls der in .webklar-preview.json angegebene index-Unterordner nicht (mehr) + // existiert, auf die Repo-Wurzel zurueckfallen statt den cp scheitern zu lassen. + try { + await fs.access(webroot) + } catch { + webroot = sourceDir + } await fs.rm(targetDir, { recursive: true, force: true }) await fs.mkdir(targetDir, { recursive: true }) await execFileAsync('cp', ['-R', `${webroot}/.`, targetDir]) + // Git-Metadaten nie mit ausliefern (verhindert Zugriff auf /.git/ im Preview). + await fs.rm(path.join(targetDir, '.git'), { recursive: true, force: true }) +} + +/** + * Platzhalter-Seite, die statt eines nackten nginx-403 ausgeliefert wird, wenn + * ein Deploy-Verzeichnis keine index.html enthaelt (z.B. leergeraeumtes Repo, + * fehlerhafter Build oder falscher index-Unterordner). So bleibt die Subdomain + * immer erreichbar und zeigt eine verstaendliche Meldung. + */ +function placeholderHtml(displayName) { + const name = String(displayName || 'Diese Website') + .replace(/&/g, '&') + .replace(//g, '>') + return ` + + + + + +${name} – wird vorbereitet + + + +
+

${name}

+

Diese Seite wird gerade vorbereitet oder aktualisiert.
Bitte in Kürze erneut laden.

+
+ + +` +} + +/** + * Stellt sicher, dass ein Deploy-Verzeichnis eine index.html besitzt. Fehlt sie, + * wird ein Platzhalter geschrieben, damit nginx (autoindex off) kein 403 liefert. + */ +async function ensureIndexHtml(targetDir, displayName) { + const indexPath = path.join(targetDir, 'index.html') + try { + await fs.access(indexPath) + return { placeholder: false } + } catch { + /* keine index.html vorhanden -> Platzhalter schreiben */ + } + await fs.mkdir(targetDir, { recursive: true }) + await fs.writeFile(indexPath, placeholderHtml(displayName), 'utf8') + return { placeholder: true } } async function deployNodeBuild(sourceDir, targetDir) { @@ -177,6 +241,10 @@ export async function runDeploy(repoFullName, branch, previewConfig) { await deployStatic(workDir, targetDir, previewConfig.index || '') } + // Universeller 403-Schutz: ohne index.html liefert nginx (autoindex off) sonst + // "403 Forbidden". Ein Platzhalter haelt die Subdomain immer erreichbar. + const index = await ensureIndexHtml(targetDir, previewConfig.displayName || subdomain) + let tls = { synced: false } try { tls = await syncPreviewTls() @@ -185,7 +253,7 @@ export async function runDeploy(repoFullName, branch, previewConfig) { tls = { synced: false, error: err.message } } - return { deployed: true, subdomain, targetDir, tls } + return { deployed: true, subdomain, targetDir, tls, placeholder: index.placeholder } } /**