8 Commits

Author SHA1 Message Date
6eea814fad feat: Add gallery management to admin panel + system documentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Features:
- Gallery upload, delete, reorder (drag & drop)
- Preview images in admin panel
- Same upload flow as events (WebP conversion via sharp)
- Complete system documentation explaining:
  - Architecture (Frontend/Backend/Volume)
  - Upload flow (Multipart → Sharp → WebP → Storage)
  - Why SQLite works on Fly.io (persistent volumes)
  - Troubleshooting guide

Admin now has full CRUD for both Events and Gallery!
2025-12-09 17:08:36 +01:00
0597c73690 Refactor Woodpecker pipeline: consolidate when conditions, replace secrets with environment for Fly.io auth.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 16:46:10 +01:00
0a2aa84a8c Refactor Woodpecker pipeline: consolidate when conditions, replace secrets with environment for Fly.io auth. 2025-12-09 16:45:28 +01:00
1120472af8 feat: Migrate old events and gallery images to persistent storage
- Add migration script to convert and copy images
- Include 7 events (Karaoke, Pub Quiz, etc.) in WebP format
- Include 9 gallery images in WebP format
- Update .gitignore to allow images in data/images/
- Add migration documentation in MIGRATION_README.md

Images are stored in backend/data/images/ which maps to
the persistent Fly.io volume at /app/data/
2025-12-09 16:41:57 +01:00
db3a38ed45 Revert "Refactor Woodpecker pipeline: consolidate when conditions, replace secrets with environment for Fly.io auth."
This reverts commit 4f8feb8652.
2025-12-09 16:36:16 +01:00
4f8feb8652 Refactor Woodpecker pipeline: consolidate when conditions, replace secrets with environment for Fly.io auth.
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is running
2025-12-09 15:56:55 +01:00
0c291079ff Test: Trigger Woodpecker CI 2025-12-09 15:56:10 +01:00
fe2f61cdc2 Simplify Woodpecker pipeline by consolidating when conditions and using secrets for Fly.io authentication 2025-12-09 15:55:51 +01:00
24 changed files with 713 additions and 22 deletions

View File

@ -1,9 +1,3 @@
when:
branch:
- main
event:
- push
steps:
deploy_frontend:
image: node:20
@ -14,14 +8,6 @@ steps:
- curl -L https://fly.io/install.sh | sh
- export PATH="$HOME/.fly/bin:$PATH"
- flyctl deploy --config fly.toml --app gallus-pub --remote-only
deploy_backend:
image: node:20
environment:
FLY_API_TOKEN:
from_secret: FLY_API_TOKEN
commands:
- cd backend
- curl -L https://fly.io/install.sh | sh
- export PATH="$HOME/.fly/bin:$PATH"
- flyctl deploy --config fly.toml --app gallus-cms-backend --remote-only
when:
branch: main
event: push

142
MIGRATION_README.md Normal file
View File

@ -0,0 +1,142 @@
# Migration der alten Events und Gallery-Bilder
## ✅ Was wurde migriert?
### Events (7 Stück):
- Karaoke (wiederkehrend)
- Pub Quiz (wiederkehrend)
- Schlager Hüttenzauber Karaoke
- Adventskalender
- Santa Karaoke-Party
- Weihnachtsferien
- Neujahrs-Apero
### Gallery-Bilder (9 Stück):
- Gallery1.webp bis Gallery9.webp
## 📁 Wo liegen die Bilder?
Alle Bilder wurden konvertiert und liegen jetzt in:
- **Events:** `backend/data/images/events/`
- **Gallery:** `backend/data/images/gallery/`
Die Bilder wurden automatisch:
- Von PNG/JPG/JPEG zu WebP konvertiert
- Auf max. 1600px Breite skaliert
- Mit 85% Qualität optimiert
## 🚀 Deployment-Schritte
### 1. Lokale Vorbereitung (bereits erledigt ✓)
- ✓ Migrations-Script erstellt
- ✓ Bilder konvertiert und in `backend/data/images/` kopiert
- ✓ Public API-Endpunkte erstellt (`/api/events/public`, `/api/gallery/public`)
- ✓ Frontend aktualisiert, um Events und Gallery dynamisch zu laden
### 2. Auf Fly.io deployen
Alle Änderungen committen und pushen:
```bash
git add .
git commit -m "feat: Migrate old events and gallery images to CMS"
git push origin main
```
Woodpecker CI wird automatisch beide Services deployen.
### 3. Nach dem ersten Deploy - Datenbank initialisieren
**Wichtig:** Die Bilder sind bereits im Repository in `backend/data/images/`, aber die Datenbank muss noch mit den Event- und Gallery-Einträgen befüllt werden.
#### Via fly ssh (Empfohlen):
```bash
# In das Backend einloggen
fly ssh console -a gallus-cms-backend
# Prüfen ob Bilder da sind
ls -la /app/data/images/events/
ls -la /app/data/images/gallery/
# Migrations-Script ausführen
cd /app
npm run migrate:old-data
```
#### Alternative: Manuell via Admin-Panel
1. Gehe zu https://gallus-pub.ch/admin
2. Melde dich an
3. Für jedes Event:
- Klicke auf "Neues Event"
- Gib Titel, Datum und Beschreibung ein
- Statt Bild hochzuladen, trage manuell die imageUrl ein:
- z.B. `/images/events/event_karaoke.webp`
- Speichere das Event
## 🔍 Verifikation
Nach dem Deployment prüfen:
1. **Frontend:** https://gallus-pub.ch/
- Events sollten angezeigt werden
- Gallery sollte Bilder zeigen
2. **Admin:** https://gallus-pub.ch/admin
- Events können bearbeitet werden
- Neue Events können hinzugefügt werden
3. **Backend Health:** https://cms.gallus-pub.ch/health
- Status sollte "ok" sein
## 📝 Event-Daten für manuelles Einfügen
Falls du die Events manuell via Admin-Panel einfügen möchtest:
### Karaoke
- **Titel:** Karaoke
- **Datum:** 2025-12-31
- **Beschreibung:** Bei uns gibt es Karaoke Mi-Sa!! <br>Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;) <br>Reserviere am besten gleich per Whatsapp <a href="tel:+41772322770">077 232 27 70</a>
- **Bild-URL:** `/images/events/event_karaoke.webp`
### Pub Quiz
- **Titel:** Pub Quiz
- **Datum:** 2025-12-31
- **Beschreibung:** Jeden Freitag findet unser <b>Pub Quiz</b> statt. Gespielt wird tischweise in 3-4 Runden. <br>Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3 <br>Auch Einzelpersonen sind herzlich willkommen! <br>*zum mitmachen minimum 1 Getränk konsumieren oder 5CHF
- **Bild-URL:** `/images/events/event_pub-quiz.webp`
### Schlager Hüttenzauber Karaoke
- **Titel:** Schlager Hüttenzauber Karaoke
- **Datum:** 2025-11-27
- **Beschreibung:** Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter <a href="tel:+41772322770">077 232 27 70</a>
- **Bild-URL:** `/images/events/event_schlager-karaoke.webp`
### Adventskalender
- **Titel:** Adventskalender
- **Datum:** 2025-12-20
- **Beschreibung:** Jeden Tag neue Überraschungen! Check unsere Social Media Stories!
- **Bild-URL:** `/images/events/event_advents-kalender.webp`
### Santa Karaoke-Party
- **Titel:** Santa Karaoke-Party
- **Datum:** 2025-12-06
- **Beschreibung:** 🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻
- **Bild-URL:** `/images/events/event_santa_karaoke.webp`
### Weihnachtsferien
- **Titel:** Weihnachtsferien
- **Datum:** 2025-12-21
- **Beschreibung:** Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀. <br> Für Anfragen WA <a href="tel:+41772322770">077 232 27 70</a> Antwort innerhalb 48h
- **Bild-URL:** `/images/events/event_ferien.webp`
### Neujahrs-Apero
- **Titel:** Neujahrs-Apero
- **Datum:** 2026-01-02
- **Beschreibung:** 18:00-20:00 Uhr
- **Bild-URL:** `/images/events/event_neujahrs-apero.webp`
## ⚠️ Wichtige Hinweise
1. **Bilder sind im Volume persistent:** Alle Bilder in `/app/data/` bleiben bei Restarts erhalten
2. **Datenbank ist persistent:** Die SQLite-DB in `/app/data/gallus_cms.db` bleibt erhalten
3. **Alte Bilder in `public/images/`:** Die alten Original-Bilder bleiben im Frontend-Repository, werden aber nicht mehr verwendet

View File

@ -45,3 +45,4 @@ All commands are run from the root of the project, from a terminal:
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
# Test commit to trigger Woodpecker

220
SYSTEM_ERKLAERUNG.md Normal file
View File

@ -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: <img src="https://cms.gallus-pub.ch/static/images/gallery/xyz.webp">
```
### 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 <volume-id> -s <new-size>
```
## 📁 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

10
backend/.gitignore vendored
View File

@ -4,7 +4,9 @@ dist
*.log
.DS_Store
/tmp
/data
*.db
*.db-wal
*.db-shm
/data/*.db
/data/*.db-wal
/data/*.db-shm
/data/workspace
# Allow images to be committed
!/data/images

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -9,7 +9,8 @@
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
"db:studio": "drizzle-kit studio",
"migrate:old-data": "tsx src/scripts/migrate-old-data.ts"
},
"dependencies": {
"@fastify/cookie": "^9.3.1",

View File

@ -0,0 +1,190 @@
import { db } from '../config/database.js';
import { events, galleryImages } from '../db/schema.js';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
// Old events data
const oldEvents = [
{
image: "/images/events/event_karaoke.jpg",
title: "Karaoke",
date: "2025-12-31", // Set as ongoing event
description: `Bei uns gibt es Karaoke Mi-Sa!! <br>
Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;) <br>
Reserviere am besten gleich per Whatsapp <a href="tel:+41772322770">077 232 27 70</a>`,
displayOrder: 0,
},
{
image: "/images/events/event_pub-quiz.jpg",
title: "Pub Quiz",
date: "2025-12-31", // Set as ongoing event
description: `Jeden Freitag findet unser <b>Pub Quiz</b> statt. Gespielt wird tischweise in 3-4 Runden. <br>
Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3 <br>
Auch Einzelpersonen sind herzlich willkommen! <br>
*zum mitmachen minimum 1 Getränk konsumieren oder 5CHF`,
displayOrder: 1,
},
{
image: "/images/events/event_schlager-karaoke.jpeg",
title: "Schlager Hüttenzauber Karaoke",
date: "2025-11-27",
description: `Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter <a href="tel:+41772322770">077 232 27 70</a>`,
displayOrder: 2,
},
{
image: "/images/events/event_advents-kalender.jpeg",
title: "Adventskalender",
date: "2025-12-20",
description: `Jeden Tag neue Überraschungen! Check unsere Social Media Stories!`,
displayOrder: 3,
},
{
image: "/images/events/event_santa_karaoke.jpeg",
title: "Santa Karaoke-Party",
date: "2025-12-06",
description: `🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻`,
displayOrder: 4,
},
{
image: "/images/events/event_ferien.jpeg",
title: "Weihnachtsferien",
date: "2025-12-21",
description: `Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀. <br> Für Anfragen WA <a href="tel:+41772322770">077 232 27 70</a> Antwort innerhalb 48h`,
displayOrder: 5,
},
{
image: "/images/events/event_neujahrs-apero.jpeg",
title: "Neujahrs-Apero",
date: "2026-01-02",
description: `18:00-20:00 Uhr`,
displayOrder: 6,
},
];
// Old gallery images
const oldGalleryImages = [
{ src: "/images/gallery/Gallery7.png", alt: "Gallery 7" },
{ src: "/images/gallery/Gallery8.png", alt: "Gallery 8" },
{ src: "/images/gallery/Gallery9.png", alt: "Gallery 9" },
{ src: "/images/gallery/Gallery6.png", alt: "Gallery 6" },
{ src: "/images/gallery/Gallery1.png", alt: "Gallery 1" },
{ src: "/images/gallery/Gallery2.png", alt: "Gallery 2" },
{ src: "/images/gallery/Gallery3.png", alt: "Gallery 3" },
{ src: "/images/gallery/Gallery4.png", alt: "Gallery 4" },
{ src: "/images/gallery/Gallery5.png", alt: "Gallery 5" },
];
async function copyAndConvertImage(
sourcePath: string,
destDir: string,
filename: string
): Promise<string> {
const projectRoot = path.join(process.cwd(), '..');
const fullSourcePath = path.join(projectRoot, 'public', sourcePath);
// Ensure destination directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const ext = path.extname(filename);
const baseName = path.basename(filename, ext);
const webpFilename = `${baseName}.webp`;
const destPath = path.join(destDir, webpFilename);
console.log(`Processing: ${fullSourcePath} -> ${destPath}`);
// Check if source exists
if (!fs.existsSync(fullSourcePath)) {
console.error(`Source file not found: ${fullSourcePath}`);
throw new Error(`Source file not found: ${fullSourcePath}`);
}
// Convert to webp and copy
await sharp(fullSourcePath)
.rotate() // Auto-rotate based on EXIF
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(destPath);
return `/images/${path.relative(destDir, destPath).replace(/\\/g, '/')}`;
}
async function migrateEvents() {
console.log('\n=== Migrating Events ===\n');
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
const eventsImageDir = path.join(dataDir, 'images', 'events');
for (const event of oldEvents) {
try {
const filename = path.basename(event.image);
const newImageUrl = await copyAndConvertImage(
event.image,
eventsImageDir,
filename
);
const [newEvent] = await db.insert(events).values({
title: event.title,
date: event.date,
description: event.description,
imageUrl: newImageUrl,
displayOrder: event.displayOrder,
isPublished: true,
}).returning();
console.log(`✓ Migrated event: ${newEvent.title}`);
} catch (error) {
console.error(`✗ Failed to migrate event "${event.title}":`, error);
}
}
}
async function migrateGallery() {
console.log('\n=== Migrating Gallery Images ===\n');
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
const galleryImageDir = path.join(dataDir, 'images', 'gallery');
for (let i = 0; i < oldGalleryImages.length; i++) {
const img = oldGalleryImages[i];
try {
const filename = path.basename(img.src);
const newImageUrl = await copyAndConvertImage(
img.src,
galleryImageDir,
filename
);
const [newImage] = await db.insert(galleryImages).values({
imageUrl: newImageUrl,
altText: img.alt,
displayOrder: i,
isPublished: true,
}).returning();
console.log(`✓ Migrated gallery image: ${newImage.altText}`);
} catch (error) {
console.error(`✗ Failed to migrate gallery image "${img.alt}":`, error);
}
}
}
async function main() {
console.log('Starting migration of old data...\n');
console.log('Working directory:', process.cwd());
console.log('Data directory:', process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'));
try {
await migrateEvents();
await migrateGallery();
console.log('\n✓ Migration completed successfully!');
} catch (error) {
console.error('\n✗ Migration failed:', error);
process.exit(1);
}
}
main();

View File

@ -68,6 +68,28 @@ const title = 'Admin';
</div>
</section>
<section id="sec-gallery" style="display:none">
<h2>Gallery verwalten</h2>
<div class="events-row">
<div class="card">
<h3>Neues Gallery-Bild</h3>
<label>Bild-Datei<input id="gal-file" type="file" accept="image/*" /></label>
<label>Alt-Text<input id="gal-alt" placeholder="Bildbeschreibung" /></label>
<button id="btn-create-gal">Bild hochladen</button>
<div id="gal-create-msg" class="muted"></div>
</div>
<div class="card" style="min-width:380px;">
<h3>Liste</h3>
<div class="toolbar">
<button id="btn-toggle-gal-reorder">Reihenfolge bearbeiten</button>
<button id="btn-save-gal-order" style="display:none">Reihenfolge speichern</button>
<span id="gal-order-msg" class="muted"></span>
</div>
<div id="gallery-list" class="grid"></div>
</div>
</div>
</section>
<section id="sec-publish" style="display:none">
<h2>Veröffentlichen</h2>
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
@ -92,9 +114,11 @@ const title = 'Admin';
document.getElementById('auth-status').textContent = `Angemeldet als ${me.user?.giteaUsername || 'Admin'}`;
// UI-Bereiche für eingeloggte Nutzer einblenden
document.getElementById('sec-events').style.display = '';
document.getElementById('sec-gallery').style.display = '';
document.getElementById('sec-publish').style.display = '';
// Direkt Events laden und auf Sektion fokussieren
await loadEvents();
await loadGallery();
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
} catch (e) {
const el = document.getElementById('auth-status');
@ -288,6 +312,131 @@ const title = 'Admin';
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
// ========== Gallery Management ==========
let galReorderMode = false;
let lastGallery = [];
async function loadGallery() {
const listEl = document.getElementById('gallery-list');
listEl.innerHTML = '<div class="muted">Lade...</div>';
try {
const data = await api('/api/gallery');
listEl.innerHTML = '';
lastGallery = (data.images || []).slice();
let renderList = lastGallery.slice();
if (galReorderMode) {
renderList.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
}
renderList.forEach((img, idx) => {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', String(galReorderMode));
card.dataset.id = img.id;
card.dataset.displayOrder = String(img.displayOrder ?? idx);
card.innerHTML = `
<div class="row" style="justify-content:space-between;align-items:center">
<div><strong>${img.altText}</strong></div>
${galReorderMode ? '<span class="drag-handle">⇅ Ziehen</span>' : ''}
</div>
<img src="${API_BASE}${img.imageUrl}" alt="${img.altText}" style="max-width:100%;height:auto;margin:0.5rem 0;" />
<div class="muted">URL: ${img.imageUrl}</div>
<div class="row-buttons">
<button data-id="${img.id}" class="btn-del-gal">Löschen</button>
</div>`;
listEl.appendChild(card);
});
listEl.querySelectorAll('.btn-del-gal').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
if (!confirm('Bild wirklich löschen?')) return;
try {
await api(`/api/gallery/${id}`, { method: 'DELETE' });
await loadGallery();
} catch(e){ alert('Fehler: '+e.message); }
})
})
if (galReorderMode) {
enableGalleryDragAndDrop(listEl);
}
} catch (e) {
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
console.error(e);
}
}
function enableGalleryDragAndDrop(container){
let draggingEl = null;
container.querySelectorAll('.card').forEach(card => {
card.addEventListener('dragstart', (e) => {
draggingEl = card; card.classList.add('dragging');
e.dataTransfer.setData('text/plain', card.dataset.id || '');
});
card.addEventListener('dragend', () => { draggingEl = null; card.classList.remove('dragging'); });
card.addEventListener('dragover', (e) => { e.preventDefault(); });
card.addEventListener('drop', (e) => {
e.preventDefault();
const target = card;
if (!draggingEl || draggingEl === target) return;
const cards = Array.from(container.querySelectorAll('.card'));
const draggingIdx = cards.indexOf(draggingEl);
const targetIdx = cards.indexOf(target);
if (draggingIdx < targetIdx) {
target.after(draggingEl);
} else {
target.before(draggingEl);
}
});
});
}
document.getElementById('btn-create-gal').addEventListener('click', async () => {
const file = document.getElementById('gal-file').files[0];
const alt = document.getElementById('gal-alt').value.trim();
const msg = document.getElementById('gal-create-msg');
if (!file) {
msg.textContent = 'Bitte wähle ein Bild aus';
return;
}
msg.textContent = 'Lade Bild hoch...';
try {
const up = await uploadImage(file, alt || 'Gallery Image');
msg.textContent = 'Bild hochgeladen';
document.getElementById('gal-file').value = '';
document.getElementById('gal-alt').value = '';
await loadGallery();
} catch(e){
msg.textContent = 'Fehler: '+e.message
}
});
document.getElementById('btn-toggle-gal-reorder').addEventListener('click', async () => {
galReorderMode = !galReorderMode;
document.getElementById('btn-save-gal-order').style.display = galReorderMode ? '' : 'none';
await loadGallery();
});
document.getElementById('btn-save-gal-order').addEventListener('click', async () => {
const container = document.getElementById('gallery-list');
const cards = Array.from(container.querySelectorAll('.card'));
const orders = cards.map((c, idx) => ({ id: c.dataset.id, displayOrder: idx }));
const msg = document.getElementById('gal-order-msg');
msg.textContent = 'Speichere Reihenfolge...';
try {
await api('/api/gallery/reorder', { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ orders }) });
msg.textContent = 'Reihenfolge gespeichert';
galReorderMode = false;
document.getElementById('btn-save-gal-order').style.display = 'none';
await loadGallery();
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
refreshAuth();
</script>
</body>