diff --git a/SYSTEM_ERKLAERUNG.md b/SYSTEM_ERKLAERUNG.md new file mode 100644 index 0000000..cb70757 --- /dev/null +++ b/SYSTEM_ERKLAERUNG.md @@ -0,0 +1,220 @@ +# 🎯 Gallus Pub CMS - System-Erklärung + +## 📐 Architektur-Überblick + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend CMS │ │ Fly.io Volume │ +│ (Astro SSG) │◄────────┤ (Fastify API) │◄────────┤ /app/data/ │ +│ gallus-pub.ch │ fetch │ cms.gallus-pub.ch│ mount │ - SQLite DB │ +│ │ │ │ │ - images/ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ +``` + +## 🔄 Wie funktioniert der Upload-Flow? + +### 1. **Admin lädt Bild hoch** (admin.astro) +``` +User wählt Bild → uploadImage() → POST /api/gallery/upload +``` + +### 2. **Backend verarbeitet Upload** (backend/src/routes/gallery.ts) +```javascript +// Line 60-134 in gallery.ts +1. Empfange Multipart-File +2. Validiere Mimetype (nur images/*) +3. Lese Stream in Buffer +4. sharp() konvertiert zu WebP: + - Auto-rotate (EXIF) + - Resize auf max 1600px + - WebP quality 82% +5. Speichere in /app/data/images/gallery/ +6. Erstelle DB-Eintrag mit imageUrl +7. Return imageUrl an Frontend +``` + +### 3. **Bilder werden serviert** (backend/src/index.ts) +```javascript +// Line 63-69 +fastifyStatic → /static/ → /app/data/ +``` + +Beispiel: +- Bild liegt in: `/app/data/images/gallery/miyma9zc-8he1di.webp` +- URL ist: `/images/gallery/miyma9zc-8he1di.webp` +- Wird serviert als: `https://cms.gallus-pub.ch/static/images/gallery/miyma9zc-8he1di.webp` + +### 4. **Frontend zeigt Bilder** (src/pages/index.astro) +```javascript +// Line 30-43 +1. Fetch von /api/gallery/public +2. Map imageUrl: `${API_BASE}${img.imageUrl}` +3. Result: https://cms.gallus-pub.ch/static/images/gallery/xyz.webp +``` + +## 💾 Warum funktioniert SQLite mit Fly.io? + +**Fly.io Volumes** sind persistente Speicher: +- Gemountet als: `/app/data/` +- Konfiguration in: `backend/fly.toml` (Line 40-42) +- Bleibt bei Restarts/Deploys erhalten + +```toml +[mounts] + source = "gallus_data" + destination = "/app/data" +``` + +### Was liegt wo? + +``` +/app/data/ +├── gallus_cms.db # SQLite Datenbank +├── gallus_cms.db-wal # Write-Ahead Log +├── gallus_cms.db-shm # Shared Memory +└── images/ + ├── events/ + │ ├── event_karaoke.webp + │ └── ... + └── gallery/ + ├── Gallery1.webp + └── ... +``` + +## 🔒 Datenfluss im Detail + +### Event erstellen: +``` +1. Admin: Bild auswählen + Formular ausfüllen +2. uploadImage(file) → /api/gallery/upload + ↓ +3. Backend: + - Sharp konvertiert → WebP + - Speichert in /app/data/images/gallery/ + - Returnt: { image: { imageUrl: "/images/gallery/xyz.webp" } } + ↓ +4. Admin: POST /api/events + Body: { title, date, description, imageUrl: "/images/gallery/xyz.webp" } + ↓ +5. Backend: + - INSERT INTO events (imageUrl = "/images/gallery/xyz.webp") + ↓ +6. Frontend: GET /api/events/public + - Fetcht Events aus DB + - Mapped imageUrl zu voller URL + - Zeigt an: +``` + +### Warum separate Uploads für Gallery? + +Events nutzen den Gallery-Upload, weil: +- Beide brauchen WebP-Konvertierung +- Beide nutzen gleichen Storage +- Vermeidet Code-Duplikation +- Gallery kann eigenständige Bildergalerie haben + +## 🎨 Admin-Panel Features + +### Events-Verwaltung: +- ✅ Event erstellen (mit Bild-Upload) +- ✅ Events auflisten +- ✅ Events löschen +- ✅ Reihenfolge ändern (Drag & Drop) +- ✅ Events veröffentlichen/verstecken (isPublished) + +### Gallery-Verwaltung: (NEU!) +- ✅ Bild hochladen +- ✅ Gallery auflisten +- ✅ Bilder löschen +- ✅ Reihenfolge ändern (Drag & Drop) +- ✅ Bilder veröffentlichen/verstecken (isPublished) + +### Publish: +- Git-Integration (für statische Seiten-Updates) +- Commit & Push zu Repository + +## 🚀 Deployment-Prozess + +### Was passiert beim Deploy? + +1. **Woodpecker CI** triggered bei Push zu `main` +2. **Frontend Deploy** (gallus-pub): + - Build Astro SSG + - Deploy zu Fly.io + +3. **Backend Deploy** (gallus-cms-backend): + - Docker Build: + ```dockerfile + # Backend-Code kompilieren + npm run build → dist/ + + # Migrierte Bilder einpacken + COPY backend/data/images → /app/migration-images + + # Migration-Script kopieren + COPY migrate-production.js → /app/ + ``` + - Deploy zu Fly.io + - **Volume bleibt erhalten** (SQLite DB + hochgeladene Bilder) + +4. **Nach erstem Deploy**: Migration ausführen + ```bash + fly ssh console -a gallus-cms-backend + node migrate-production.js + ``` + - Kopiert Bilder: `/app/migration-images/` → `/app/data/images/` + - Befüllt DB mit Event/Gallery-Einträgen + +## 🔍 Troubleshooting + +### "Bilder werden nicht angezeigt" + +**Prüfe:** +1. Backend logs: `fly logs -a gallus-cms-backend` +2. Bild existiert: `fly ssh console -a gallus-cms-backend` + ```bash + ls -la /app/data/images/gallery/ + ``` +3. DB-Eintrag korrekt: + ```bash + sqlite3 /app/data/gallus_cms.db + SELECT * FROM gallery_images; + ``` +4. Static-Route funktioniert: + ```bash + curl https://cms.gallus-pub.ch/static/images/gallery/xyz.webp + ``` + +### "SQLite locked" Fehler + +- Nur ein Writer zur Zeit erlaubt +- Bei hohem Traffic: Wechsel zu PostgreSQL empfohlen +- Für Gallus Pub: ausreichend (wenig Writes) + +### "Volume voll" + +```bash +fly volumes list -a gallus-cms-backend +fly volumes extend -s +``` + +## 📁 Wichtige Dateien + +| Datei | Zweck | +|-------|-------| +| `backend/src/routes/gallery.ts` | Gallery-Upload & CRUD | +| `backend/src/routes/events.ts` | Events CRUD + Public API | +| `backend/src/index.ts` | Static File Serving | +| `backend/migrate-production.js` | Initiale Daten-Migration | +| `src/pages/admin.astro` | Admin-Interface | +| `src/pages/index.astro` | Frontend (fetched von API) | +| `backend/fly.toml` | Backend Fly.io Config | +| `fly.toml` | Frontend Fly.io Config | + +## 🎯 Nächste Schritte + +1. ✅ Gallery-Verwaltung implementiert +2. ⏳ Migration ausführen (nach Deploy) +3. ⏳ Testen: Bild hochladen → Frontend anzeigen +4. 📋 Optional: Image-Editing Features +5. 📋 Optional: Bulk-Upload für Gallery diff --git a/src/pages/admin.astro b/src/pages/admin.astro index 8570387..c52ceab 100644 --- a/src/pages/admin.astro +++ b/src/pages/admin.astro @@ -68,6 +68,28 @@ const title = 'Admin'; + +