diff --git a/app/data/site.config.json b/app/data/site.config.json index 50dba86..d225c69 100644 --- a/app/data/site.config.json +++ b/app/data/site.config.json @@ -1,5 +1,4 @@ { - "updated_at": "2026-08-09T21:48:17.969Z", "siteName": "N&D IT Solutions", "metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.", "is_demo_mode": false, @@ -12,119 +11,13 @@ "glassEffect": true }, "footer": { - "brandDescription": "Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.", + "brandDescription": "Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.", "copyrightText": "N&D i-t SOLUTIONS. Alle Rechte vorbehalten.", "showLegalLinks": true }, - "hero": { - "badge": "š Waas Enterprise Baukastensystem", - "title": "Maßgeschneiderte Webseiten. Digital im Griff.", - "subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis – inklusive Wartung & Hosting.", - "ctaPrimaryText": "Projekt anfragen", - "ctaPrimaryLink": "#contact", - "ctaSecondaryText": "Mehr erfahren", - "ctaSecondaryLink": "#services", - "elements": [ - { - "id": "el-1", - "slotId": "hero-badge-slot", - "type": "badge", - "content": "WaaS Engine" - }, - { - "id": "el-2", - "slotId": "hero-actions-slot", - "type": "button", - "content": "Neuer Button", - "link": "#contact", - "animation0": "spring-fade", - "style": "primary" - } - ] - }, - "bento": { - "sectionTitle": "High-End Features & Leistung", - "sectionSubtitle": "Modernste Technologie & Höchster Datenschutz vereint.", - "cards": [ - { - "id": "card-1", - "title": "Ultra Fast SSR", - "description": "Sub-100ms Ladezeiten dank Astro Hybrid SSR Architecture. Google Core Web Vitals > 95%.", - "colSpan": 2, - "badge": "Performance", - "icon": "zap" - }, - { - "id": "card-2", - "title": "100% DSGVO-Sicher", - "description": "Keine Cookies, keine Tracking-Skripte, Zero Third-Party Requests.", - "colSpan": 1, - "badge": "Datenschutz", - "icon": "shield" - }, - { - "id": "card-3", - "title": "Liquid Glass UI", - "description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physikalischen Kanten-Effekten.", - "colSpan": 1, - "badge": "Design", - "icon": "layers" - }, - { - "id": "card-4", - "title": "Sorgenfrei-Paket", - "description": "Hosting, SSL, Wartung und regelmäßige Inhalts-Updates komplett inklusive.", - "colSpan": 2, - "badge": "Full-Service", - "icon": "check" - } - ] - }, - "contact": { - "title": "Bereit für Ihr Projekt?", - "subtitle": "Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich.", - "recipientEmail": "kontakt@ndsolutions.de", - "phone": "+49 (0) 123 456789", - "address": "Deutschland" - }, - "sectionsOrder": [ - "hero", - "bento", - "contact" - ], - "sectionsDetails": [ - { - "id": "hero", - "type": "hero", - "title": "Hero Sektion", - "elements": [ - { - "id": "el-1", - "slotId": "hero-badge-slot", - "type": "badge", - "content": "WaaS Engine" - } - ] - }, - { - "id": "bento", - "type": "bento", - "title": "Bento Grid", - "elements": [] - }, - { - "id": "contact", - "type": "contact", - "title": "Contact Section", - "elements": [] - } - ], "site_info": { "title": "N&D IT Solutions", - "homepage_id": "page_home", - "email": "", - "phone": "", - "address": "" + "homepage_id": "page_home" }, "pages": [ { @@ -132,39 +25,36 @@ "slug": "/", "title": "Startseite (Hauptseite)", "is_published": true, - "sections": [] - }, - { - "id": "page_templates", - "slug": "/templates", - "title": "Templates & Design-Muster", - "is_published": true, "sections": [ { - "id": "hero_templates", + "id": "sec_hero_1", "type": "HeroSection", "settings": { - "title": "Templates", - "subtitle": "Unsere fertigen Design-Vorlagen" + "title": "Maßgeschneiderte Webseiten. Digital im Griff.", + "subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen.", + "ctaPrimaryText": "Projekt anfragen", + "ctaPrimaryLink": "#contact", + "bg_color": "#0b0f19" } - } - ] - }, - { - "id": "page_privacy", - "slug": "/datenschutz", - "title": "Datenschutzerklärung", - "is_published": true, - "sections": [ + }, { - "id": "privacy_content", - "type": "HeroSection", + "id": "sec_bento_1", + "type": "BentoGrid", "settings": { - "title": "Datenschutz", - "subtitle": "100% DSGVO-konforme Datenverarbeitung." + "sectionTitle": "High-End Features & Leistung", + "sectionSubtitle": "Modernste Technologie & Höchster Datenschutz vereint." + } + }, + { + "id": "sec_contact_1", + "type": "ContactSection", + "settings": { + "title": "Bereit für Ihr Projekt?", + "subtitle": "Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich." } } ] } - ] + ], + "updated_at": "2026-08-09T22:08:57.203Z" } \ No newline at end of file diff --git a/app/data/waas.db b/app/data/waas.db new file mode 100644 index 0000000..947edab Binary files /dev/null and b/app/data/waas.db differ diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ffa1fcd..85b27ca 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -8,6 +8,11 @@ echo "🚀 [WaaS Container Entrypoint] Prüfe Daten-Volume unter ${DATA_DIR}..." # 1. Sicherstellen, dass das Volume-Verzeichnis existiert mkdir -p "$DATA_DIR" +# 1.5 SQLite DB initialisieren (falls nicht vorhanden) +if [ ! -f "$DATA_DIR/waas.db" ]; then + echo "🗄️ Keine SQLite-Datenbank waas.db gefunden. Initialisierung wird beim Anwendungsstart durchgeführt..." +fi + # 2. site.config.json Seeding if [ ! -f "$DATA_DIR/site.config.json" ]; then echo "📄 Keine site.config.json gefunden. Erstelle Standard-Konfiguration..." diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..09255c6 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,132 @@ +import { DatabaseSync } from 'node:sqlite'; +import path from 'node:path'; +import fs from 'node:fs'; + +const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); +const DB_PATH = path.join(DATA_DIR, 'waas.db'); + +let dbInstance: DatabaseSync | null = null; + +export function getDb(): DatabaseSync { + if (dbInstance) return dbInstance; + + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + dbInstance = new DatabaseSync(DB_PATH); + initSchema(dbInstance); + return dbInstance; +} + +function initSchema(db: DatabaseSync) { + // 1. site_settings + db.exec(` + CREATE TABLE IF NOT EXISTS site_settings ( + key TEXT PRIMARY KEY, + value_json TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); + + // 2. pages + db.exec(` + CREATE TABLE IF NOT EXISTS pages ( + id TEXT PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'published', + homepage_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); + + // 3. page_sections + db.exec(` + CREATE TABLE IF NOT EXISTS page_sections ( + id TEXT PRIMARY KEY, + page_id TEXT NOT NULL, + section_type TEXT NOT NULL, + order_index INTEGER NOT NULL DEFAULT 0, + content_draft_json TEXT NOT NULL, + content_published_json TEXT NOT NULL, + styles_json TEXT, + FOREIGN KEY (page_id) REFERENCES pages (id) ON DELETE CASCADE + ); + `); + + // Initial Seeding falls DB leer + seedInitialData(db); +} + +function seedInitialData(db: DatabaseSync) { + const checkPages = db.prepare('SELECT COUNT(*) as count FROM pages').get() as { count: number }; + if (checkPages.count > 0) return; + + const now = new Date().toISOString(); + + // Seed site_settings + const initialSettings = { + siteName: 'N&D IT Solutions', + metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.', + is_demo_mode: false, + header: { + brandName: 'N&D', + brandHighlight: 'i-t SOLUTIONS', + ctaText: 'Erstgespräch buchen', + ctaLink: '#contact', + showThemeToggle: true, + glassEffect: true + }, + footer: { + brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.', + copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.', + showLegalLinks: true + }, + site_info: { + title: 'N&D IT Solutions', + homepage_id: 'page_home' + } + }; + + db.prepare('INSERT INTO site_settings (key, value_json, updated_at) VALUES (?, ?, ?)').run( + 'site_config', + JSON.stringify(initialSettings), + now + ); + + // Seed default pages & sections + db.prepare('INSERT INTO pages (id, slug, title, status, homepage_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run( + 'page_home', + '/', + 'Startseite (Hauptseite)', + 'published', + 'page_home', + now, + now + ); + + const heroDraft = JSON.stringify({ + title: 'Maßgeschneiderte Webseiten. Digital im Griff.', + subtitle: 'Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen.', + ctaPrimaryText: 'Projekt anfragen', + ctaPrimaryLink: '#contact' + }); + + const bentoDraft = JSON.stringify({ + sectionTitle: 'High-End Features & Leistung', + sectionSubtitle: 'Modernste Technologie & Höchster Datenschutz vereint.' + }); + + const contactDraft = JSON.stringify({ + title: 'Bereit für Ihr Projekt?', + subtitle: 'Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich.' + }); + + const stmt = db.prepare('INSERT INTO page_sections (id, page_id, section_type, order_index, content_draft_json, content_published_json, styles_json) VALUES (?, ?, ?, ?, ?, ?, ?)'); + + stmt.run('sec_hero_1', 'page_home', 'HeroSection', 0, heroDraft, heroDraft, JSON.stringify({ bg_color: '#0b0f19' })); + stmt.run('sec_bento_1', 'page_home', 'BentoGrid', 1, bentoDraft, bentoDraft, JSON.stringify({})); + stmt.run('sec_contact_1', 'page_home', 'ContactSection', 2, contactDraft, contactDraft, JSON.stringify({})); +} diff --git a/src/pages/api/editor/add-section.ts b/src/pages/api/editor/add-section.ts index 810b1fd..88233e8 100644 --- a/src/pages/api/editor/add-section.ts +++ b/src/pages/api/editor/add-section.ts @@ -1,40 +1,24 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { getDb } from '../../../db/index'; +import { generateDraftConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - -// Vordefinierte Standard-Templates für neue Sektionen +// Standard-Templates const SECTION_TEMPLATES: Record = { HeroSection: { - type: 'HeroSection', - settings: { - title: 'Neue Überschrift', - subtitle: 'Beschreibungstext hier eingeben.', - cta_text: 'Jetzt anfragen', - cta_url: '#contact' - } + title: 'Neue Überschrift', + subtitle: 'Beschreibungstext hier eingeben.', + ctaPrimaryText: 'Jetzt anfragen', + ctaPrimaryLink: '#contact' }, BentoGrid: { - type: 'BentoGrid', - settings: { - title: 'Unsere Highlights', - columns: 3, - items: [ - { title: 'Feature 1', description: 'Beschreibung 1' }, - { title: 'Feature 2', description: 'Beschreibung 2' }, - { title: 'Feature 3', description: 'Beschreibung 3' } - ] - } + sectionTitle: 'Unsere Highlights', + sectionSubtitle: 'Modernste Features im Überblick' }, ContactSection: { - type: 'ContactSection', - settings: { - title: 'Kontaktieren Sie uns', - email_recipient: 'info@kunden-domain.de' - } + title: 'Kontaktieren Sie uns', + subtitle: 'Wir antworten innerhalb von 24 Stunden.' } }; @@ -42,40 +26,32 @@ export const POST: APIRoute = async ({ request }) => { try { const { page_id, section_type } = await request.json(); - if (!page_id || !section_type || !SECTION_TEMPLATES[section_type]) { + if (!page_id || !section_type) { return new Response(JSON.stringify({ error: 'Gültige page_id und section_type erforderlich' }), { status: 400 }); } - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + const db = getDb(); + const secId = `sec_${section_type.toLowerCase()}_${Date.now()}`; + const defaultContent = JSON.stringify(SECTION_TEMPLATES[section_type] || {}); - let configRaw: string; - try { - configRaw = await fs.readFile(draftPath, 'utf-8'); - } catch { - configRaw = await fs.readFile(livePath, 'utf-8'); - } + // Aktuellen max order_index ermitteln + const maxOrderRow = db.prepare('SELECT MAX(order_index) as max_idx FROM page_sections WHERE page_id = ?').get(page_id) as { max_idx: number | null }; + const nextIdx = (maxOrderRow?.max_idx ?? -1) + 1; - const config = JSON.parse(configRaw); - const targetPage = config.pages?.find((p: any) => p.id === page_id); + db.prepare('INSERT INTO page_sections (id, page_id, section_type, order_index, content_draft_json, content_published_json, styles_json) VALUES (?, ?, ?, ?, ?, ?, ?)').run( + secId, + page_id, + section_type, + nextIdx, + defaultContent, + defaultContent, + JSON.stringify({}) + ); - if (!targetPage) { - return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 }); - } + // Draft-Config via File-Generator neu kompilieren + await generateDraftConfig(); - // Neue Sektion mit eindeutiger ID anfügen - const newSection = { - id: `sec_${section_type.toLowerCase()}_${Date.now()}`, - ...SECTION_TEMPLATES[section_type] - }; - - targetPage.sections = targetPage.sections || []; - targetPage.sections.push(newSection); - - await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); - - return new Response(JSON.stringify({ success: true, section: newSection }), { status: 201 }); + return new Response(JSON.stringify({ success: true, section_id: secId }), { status: 201 }); } catch (error) { return new Response(JSON.stringify({ error: 'Fehler beim Hinzufügen der Sektion' }), { status: 500 }); } diff --git a/src/pages/api/editor/delete-section.ts b/src/pages/api/editor/delete-section.ts index 55cdb37..70b8046 100644 --- a/src/pages/api/editor/delete-section.ts +++ b/src/pages/api/editor/delete-section.ts @@ -1,11 +1,9 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { getDb } from '../../../db/index'; +import { generateDraftConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - export const POST: APIRoute = async ({ request }) => { try { const { page_id, section_id } = await request.json(); @@ -14,28 +12,11 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'page_id und section_id erforderlich' }), { status: 400 }); } - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + const db = getDb(); + db.prepare('DELETE FROM page_sections WHERE id = ? AND page_id = ?').run(section_id, page_id); - let configRaw: string; - try { - configRaw = await fs.readFile(draftPath, 'utf-8'); - } catch { - configRaw = await fs.readFile(livePath, 'utf-8'); - } - - const config = JSON.parse(configRaw); - const targetPage = config.pages?.find((p: any) => p.id === page_id); - - if (!targetPage) { - return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 }); - } - - // Sektion filtern / entfernen - targetPage.sections = targetPage.sections.filter((sec: any) => sec.id !== section_id); - - await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); + // Draft-Config neu kompilieren + await generateDraftConfig(); return new Response(JSON.stringify({ success: true, message: 'Sektion gelöscht' }), { status: 200 }); } catch (error) { diff --git a/src/pages/api/editor/publish.ts b/src/pages/api/editor/publish.ts index 2f71e79..d44d28e 100644 --- a/src/pages/api/editor/publish.ts +++ b/src/pages/api/editor/publish.ts @@ -1,27 +1,13 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { publishLiveConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - export const POST: APIRoute = async () => { try { - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + const liveConfig = await publishLiveConfig(); - // Prüfe ob ein Entwurf existiert - try { - await fs.access(draftPath); - } catch { - return new Response(JSON.stringify({ error: 'Kein gespeicherter Entwurf vorhanden' }), { status: 404 }); - } - - // Atomares Kopieren des Entwurfs auf die Live-Konfiguration - await fs.copyFile(draftPath, livePath); - - return new Response(JSON.stringify({ success: true, message: 'Website erfolgreich veröffentlicht!' }), { status: 200 }); + return new Response(JSON.stringify({ success: true, message: 'Website erfolgreich veröffentlicht!', config: liveConfig }), { status: 200 }); } catch (error) { return new Response(JSON.stringify({ error: 'Fehler beim Veröffentlichen' }), { status: 500 }); } diff --git a/src/pages/api/editor/save-draft.ts b/src/pages/api/editor/save-draft.ts index a1d3097..5491240 100644 --- a/src/pages/api/editor/save-draft.ts +++ b/src/pages/api/editor/save-draft.ts @@ -1,36 +1,17 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { generateDraftConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - export const POST: APIRoute = async ({ request }) => { try { const draftBody = await request.json(); - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + + // Generiert aktualisierte site.config.draft.json + const draftConfig = await generateDraftConfig(); - let baseConfig: any = {}; - try { - const raw = await fs.readFile(draftPath, 'utf-8'); - baseConfig = JSON.parse(raw); - } catch { - try { - const rawLive = await fs.readFile(livePath, 'utf-8'); - baseConfig = JSON.parse(rawLive); - } catch {} - } - - const updatedConfig = { ...baseConfig, ...draftBody, updated_at: new Date().toISOString() }; - - await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(draftPath, JSON.stringify(updatedConfig, null, 2), 'utf-8'); - - return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert' }), { status: 200 }); + return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert', config: draftConfig }), { status: 200 }); } catch (error) { return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 }); } }; - diff --git a/src/pages/api/editor/update-section-settings.ts b/src/pages/api/editor/update-section-settings.ts index eb1ff4e..c57b7e9 100644 --- a/src/pages/api/editor/update-section-settings.ts +++ b/src/pages/api/editor/update-section-settings.ts @@ -1,11 +1,9 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { getDb } from '../../../db/index'; +import { generateDraftConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - export const POST: APIRoute = async ({ request }) => { try { const { page_id, section_id, settings } = await request.json(); @@ -14,35 +12,43 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'page_id, section_id und settings erforderlich' }), { status: 400 }); } - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + const db = getDb(); + const existingRow = db.prepare('SELECT content_draft_json, styles_json FROM page_sections WHERE id = ?').get(section_id) as any; - let configRaw: string; - try { - configRaw = await fs.readFile(draftPath, 'utf-8'); - } catch { - configRaw = await fs.readFile(livePath, 'utf-8'); - } - - const config = JSON.parse(configRaw); - const targetPage = config.pages?.find((p: any) => p.id === page_id); - - if (!targetPage) { - return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 }); - } - - // Sektion finden und Einstellungen aktualisieren - const targetSection = targetPage.sections?.find((sec: any) => sec.id === section_id); - if (!targetSection) { + if (!existingRow) { return new Response(JSON.stringify({ error: 'Sektion nicht gefunden' }), { status: 404 }); } - targetSection.settings = { ...targetSection.settings, ...settings }; + const currentDraft = JSON.parse(existingRow.content_draft_json || '{}'); + const currentStyles = JSON.parse(existingRow.styles_json || '{}'); - await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); + // Trenne Styles (z. B. bg_color, accent_color) von typischem Inhalt + const { bg_color, accent_color, animation, backdrop_blur, liquid_edge, padding_top, padding_right, padding_bottom, padding_left, ...contentFields } = settings; + + const updatedDraft = { ...currentDraft, ...contentFields }; + const updatedStyles = { + ...currentStyles, + ...(bg_color !== undefined ? { bg_color } : {}), + ...(accent_color !== undefined ? { accent_color } : {}), + ...(animation !== undefined ? { animation } : {}), + ...(backdrop_blur !== undefined ? { backdrop_blur } : {}), + ...(liquid_edge !== undefined ? { liquid_edge } : {}), + ...(padding_top !== undefined ? { padding_top } : {}), + ...(padding_right !== undefined ? { padding_right } : {}), + ...(padding_bottom !== undefined ? { padding_bottom } : {}), + ...(padding_left !== undefined ? { padding_left } : {}) + }; - return new Response(JSON.stringify({ success: true, settings: targetSection.settings }), { status: 200 }); + db.prepare('UPDATE page_sections SET content_draft_json = ?, styles_json = ? WHERE id = ?').run( + JSON.stringify(updatedDraft), + JSON.stringify(updatedStyles), + section_id + ); + + // Draft-Config neu kompilieren + await generateDraftConfig(); + + return new Response(JSON.stringify({ success: true, settings: { ...updatedDraft, ...updatedStyles } }), { status: 200 }); } catch (error) { return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektions-Einstellungen' }), { status: 500 }); } diff --git a/src/pages/api/editor/update-section.ts b/src/pages/api/editor/update-section.ts index d3e3008..a9f1803 100644 --- a/src/pages/api/editor/update-section.ts +++ b/src/pages/api/editor/update-section.ts @@ -1,11 +1,9 @@ import type { APIRoute } from 'astro'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import { getDb } from '../../../db/index'; +import { generateDraftConfig } from '../../../services/file-generator'; export const prerender = false; -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - export const POST: APIRoute = async ({ request }) => { try { const { page_id, sections } = await request.json(); @@ -14,30 +12,16 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'page_id und sections-Array erforderlich' }), { status: 400 }); } - const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); - const livePath = path.join(DATA_DIR, 'site.config.json'); + const db = getDb(); + const updateStmt = db.prepare('UPDATE page_sections SET order_index = ?, content_draft_json = ? WHERE id = ?'); - // 1. Lade bestehende Config (Entwurf bevorzugt) - let configRaw: string; - try { - configRaw = await fs.readFile(draftPath, 'utf-8'); - } catch { - configRaw = await fs.readFile(livePath, 'utf-8'); - } + sections.forEach((sec, index) => { + const draftJson = JSON.stringify(sec.settings || sec.content || {}); + updateStmt.run(index, draftJson, sec.id); + }); - const config = JSON.parse(configRaw); - - // 2. Finde die Zielseite und aktualisiere das sections-Array - const targetPage = config.pages?.find((p: any) => p.id === page_id); - if (!targetPage) { - return new Response(JSON.stringify({ error: 'Zielseite nicht gefunden' }), { status: 404 }); - } - - targetPage.sections = sections; - - // 3. Speichere ausschließlich in site.config.draft.json - await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); + // Draft-Config neu kompilieren + await generateDraftConfig(); return new Response( JSON.stringify({ success: true, message: 'Sektionen im Entwurf aktualisiert' }), diff --git a/src/services/file-generator.ts b/src/services/file-generator.ts new file mode 100644 index 0000000..647c3cf --- /dev/null +++ b/src/services/file-generator.ts @@ -0,0 +1,98 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { getDb } from '../db/index'; + +const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); + +export async function generateDraftConfig(): Promise { + const db = getDb(); + + // 1. Globale Settings laden + const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined; + const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {}; + + // 2. Alle Seiten laden + const pagesRows = db.prepare('SELECT * FROM pages ORDER BY created_at ASC').all() as any[]; + + const pages = pagesRows.map((page) => { + const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[]; + + const sections = sectionsRows.map((sec) => { + const draftContent = JSON.parse(sec.content_draft_json || '{}'); + const styles = JSON.parse(sec.styles_json || '{}'); + return { + id: sec.id, + type: sec.section_type, + settings: { ...draftContent, ...styles } + }; + }); + + return { + id: page.id, + slug: page.slug, + title: page.title, + is_published: page.status === 'published', + sections + }; + }); + + const fullDraftConfig = { + ...baseConfig, + pages, + updated_at: new Date().toISOString() + }; + + const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); + await fs.mkdir(DATA_DIR, { recursive: true }); + await fs.writeFile(draftPath, JSON.stringify(fullDraftConfig, null, 2), 'utf-8'); + + return fullDraftConfig; +} + +export async function publishLiveConfig(): Promise { + const db = getDb(); + + // 1. In DB: Setze content_published_json = content_draft_json für alle Sektionen + db.exec('UPDATE page_sections SET content_published_json = content_draft_json'); + + // 2. Globale Settings laden + const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined; + const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {}; + + // 3. Alle veröffentlichten Seiten laden + const pagesRows = db.prepare('SELECT * FROM pages WHERE status = ? ORDER BY created_at ASC').all('published') as any[]; + + const pages = pagesRows.map((page) => { + const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[]; + + const sections = sectionsRows.map((sec) => { + const pubContent = JSON.parse(sec.content_published_json || '{}'); + const styles = JSON.parse(sec.styles_json || '{}'); + return { + id: sec.id, + type: sec.section_type, + settings: { ...pubContent, ...styles } + }; + }); + + return { + id: page.id, + slug: page.slug, + title: page.title, + is_published: true, + sections + }; + }); + + const fullLiveConfig = { + ...baseConfig, + pages, + updated_at: new Date().toISOString() + }; + + const livePath = path.join(DATA_DIR, 'site.config.json'); + await fs.mkdir(DATA_DIR, { recursive: true }); + await fs.writeFile(livePath, JSON.stringify(fullLiveConfig, null, 2), 'utf-8'); + + return fullLiveConfig; +} diff --git a/tests/fileGenerator.test.ts b/tests/fileGenerator.test.ts new file mode 100644 index 0000000..f13505b --- /dev/null +++ b/tests/fileGenerator.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { getDb } from '../src/db/index'; +import { generateDraftConfig, publishLiveConfig } from '../src/services/file-generator'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +describe('DB-Driven File Generation Service Test', () => { + it('soll Entwurf aus DB lesen und site.config.draft.json generieren', async () => { + const draftConfig = await generateDraftConfig(); + expect(draftConfig).toBeDefined(); + expect(draftConfig.pages).toBeDefined(); + expect(Array.isArray(draftConfig.pages)).toBe(true); + + const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); + const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); + const exists = await fs.access(draftPath).then(() => true).catch(() => false); + expect(exists).toBe(true); + }); + + it('soll Live-Config aus DB freigeben und site.config.json atomar erstellen', async () => { + const liveConfig = await publishLiveConfig(); + expect(liveConfig).toBeDefined(); + expect(liveConfig.pages).toBeDefined(); + + const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); + const livePath = path.join(DATA_DIR, 'site.config.json'); + const exists = await fs.access(livePath).then(() => true).catch(() => false); + expect(exists).toBe(true); + }); +});