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/
142
MIGRATION_README.md
Normal 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
|
||||||
10
backend/.gitignore
vendored
@ -4,7 +4,9 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/tmp
|
/tmp
|
||||||
/data
|
/data/*.db
|
||||||
*.db
|
/data/*.db-wal
|
||||||
*.db-wal
|
/data/*.db-shm
|
||||||
*.db-shm
|
/data/workspace
|
||||||
|
# Allow images to be committed
|
||||||
|
!/data/images
|
||||||
|
|||||||
BIN
backend/data/images/events/event_advents-kalender.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
backend/data/images/events/event_ferien.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
backend/data/images/events/event_karaoke.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
backend/data/images/events/event_neujahrs-apero.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
backend/data/images/events/event_pub-quiz.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
backend/data/images/events/event_santa_karaoke.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
backend/data/images/events/event_schlager-karaoke.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
backend/data/images/gallery/Gallery1.webp
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
backend/data/images/gallery/Gallery2.webp
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
backend/data/images/gallery/Gallery3.webp
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
backend/data/images/gallery/Gallery4.webp
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
backend/data/images/gallery/Gallery5.webp
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
backend/data/images/gallery/Gallery6.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
backend/data/images/gallery/Gallery7.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
backend/data/images/gallery/Gallery8.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
backend/data/images/gallery/Gallery9.webp
Normal file
|
After Width: | Height: | Size: 162 KiB |
@ -9,7 +9,8 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"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": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
|
|||||||
190
backend/src/scripts/migrate-old-data.ts
Normal 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();
|
||||||