feat: implement DB-driven architecture with SQLite persistence and file-generator compilation service
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 50s
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 50s
This commit is contained in:
@@ -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"
|
||||
}
|
||||
BIN
app/data/waas.db
Normal file
BIN
app/data/waas.db
Normal file
Binary file not shown.
@@ -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..."
|
||||
|
||||
132
src/db/index.ts
Normal file
132
src/db/index.ts
Normal file
@@ -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({}));
|
||||
}
|
||||
@@ -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<string, any> = {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
98
src/services/file-generator.ts
Normal file
98
src/services/file-generator.ts
Normal file
@@ -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<any> {
|
||||
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<any> {
|
||||
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;
|
||||
}
|
||||
30
tests/fileGenerator.test.ts
Normal file
30
tests/fileGenerator.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user