84 Commits

Author SHA1 Message Date
387ef209ab Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-09 13:14:23 +00:00
3b27cbd194 chore(woodpecker): simplify audit file handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Removed redundant `/tmp/` paths for audit result and output files.
- Ensured consistent file access in vulnerability checks and Discord notifications.
- Added workspace file listing for better debugging in case of missing audit results.
2026-01-07 16:47:03 +01:00
61842ebc70 refactor(woodpecker): improve commit message handling for payload preparation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Redirected commit messages to a temporary file to handle special characters safely.
- Adjusted jq payload process for better reliability.
2026-01-07 16:41:58 +01:00
4e2418116f feat(woodpecker): enhance npm audit and Discord notification steps
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Refined npm audit process to generate detailed JSON and text outputs.
- Improved Discord notifications with comprehensive vulnerability details and formatting.
- Replaced `apt-get` with `apk` for faster lightweight image handling.
2026-01-07 16:38:15 +01:00
c1fd535549 refactor(woodpecker): streamline Discord payload handling with temporary file usage
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Simplified payload preparation by redirecting commit messages to a temporary file.
- Ensured cleanup with `rm -f` for improved reliability and maintainability.
2026-01-07 16:34:25 +01:00
78f5da9cff feat(woodpecker): add Discord notifications for build status
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Implemented success and failure notifications using `jq` for secure payload formatting.
- Enhanced YAML to manage build alerts and improve CI visibility.
2026-01-07 16:32:00 +01:00
b283816713 refactor(schema): remove redundant comment from users table definition
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-07 16:27:58 +01:00
c77bf3e757 Update events 2026-01-07 15:03:52 +00:00
k
36b2053642 Add dependency audit step to CI and update package dependencies. 2026-01-07 12:35:54 +01:00
c3898170fd Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-02 11:46:37 +00:00
4cc1b21c05 Reorder components in file-generator.service.ts to display Hero above Banner
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-24 14:27:47 +01:00
e41334a7cc Reorder components in index.astro to display Hero above Banner 2025-12-24 14:27:16 +01:00
47743e9239 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-24 12:55:28 +00:00
d271378912 feat: Add Banner component to file generator output
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Integrated `Banner.astro` into generated file layout for consistent use across components.
2025-12-22 23:06:48 +01:00
4cc6c4f210 refactor(Banner): remove redundant debug logs in loadBanner function
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-22 23:01:25 +01:00
fde4adfad5 feat: Add Banner component across Gallery, Openings, and Homepage layouts
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Integrated `Banner.astro` into `Gallery.astro`, `Openings.astro`, and `index.astro` for consistent banner display.
2025-12-22 22:57:28 +01:00
3e530e0ac5 Merge remote-tracking branch 'origin/main'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-22 22:44:39 +01:00
d8153ed619 feat(Banner): enhance loading logic and add detailed debug logs
- Updated `loadBanner` to wait for DOM readiness with `DOMContentLoaded` support.
- Added comprehensive debug logs for banner loading, response status, and DOM interactions.
2025-12-22 22:44:22 +01:00
3df25da009 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-18 13:21:25 +00:00
a181993ed5 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-18 12:59:40 +00:00
c289541cd5 refactor(cors): simplify origin validation logic in index.ts
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Streamlined CORS logic by consolidating `allowedOrigins` checks and improving readability.
- Updated callback invocations for consistency and clarity.
2025-12-18 13:54:41 +01:00
c9d067b1e3 feat: Support multiple CORS origins and enhance origin validation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Updated `fly.toml` to allow multiple CORS origins.
- Refactored CORS logic in `index.ts` to validate and support multiple origins, including handling requests with no origin.
2025-12-18 13:51:29 +01:00
4533f6cc3d Reorder components in index.astro to display Hero before Banner
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-18 13:30:01 +01:00
4f12ebaa9a Refactor: Update banner sorting logic to prioritize relevance
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-18 13:24:22 +01:00
a7d53ffe21 feat: Improve banner fetching logic and integrate Banner component
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Adjusted server logic in `/banners/active` to resolve timezone issues and ensure consistent date handling.
- Sorted active banners by creation date in descending order for better relevance.
- Integrated `Banner.astro` component into the homepage layout for displaying active banners.
2025-12-18 13:16:49 +01:00
c723e4919d chore: Remove outdated comment in auth route JSON schema definition
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-17 21:51:22 +01:00
f8cbc60a60 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-17 20:10:35 +00:00
feec8ed314 feat: Add backend routes and styles for banner management
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced `banners.ts` with CRUD operations for managing banners.
- Added `/banners/active` endpoint to fetch active banners.
- Secured admin-only routes for banner creation, update, and deletion.
- Created `Banner.css` for banner styling.
2025-12-17 21:02:00 +01:00
2b64a21f16 feat: Add Banner component for fetching and displaying active banners
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Implemented `Banner.astro` component to retrieve active banners from the CMS.
- Integrated styling via `Banner.css`.
- Handles errors gracefully during banner fetch.
2025-12-17 20:59:23 +01:00
20feee84a6 Jetzt wird in der Event-Liste das Bild als Vorschau angezeigt mit dem Event-Titel als Alt-Text
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-17 20:54:20 +01:00
bf7e38ba2d feat: Add banner management feature and improve event/gallery image handling
- Introduced a new "Banners" feature, enabling banner creation, management, and display across the admin panel and frontend.
- Enhanced image handling for events and gallery by converting images to optimized webp format.
- Added `banners` table in the database schema for storing announcements.
- Integrated new `/api/banners` route in backend for banner operations.
- Updated `index.astro` to include banner display component.
- Added supporting UI and APIs in the admin panel for banner management.
2025-12-17 20:47:38 +01:00
d0101b2974 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-15 16:38:55 +00:00
75f0c41e5c Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-15 16:34:32 +00:00
ffadf378f9 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 16:32:41 +00:00
5e7425cadf Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 21:06:33 +00:00
7a067f60ff Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 21:03:20 +00:00
980200f963 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:50:25 +00:00
de278fab29 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:45:37 +00:00
160d384143 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-14 20:45:04 +00:00
f440cbb7f3 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:40:57 +00:00
72faefc88d Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:36:20 +00:00
e4ada94390 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-14 20:34:31 +00:00
4eab0e6dd2 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:31:47 +00:00
6222d5f19c Revert "images fixing with database saves"
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This reverts commit c45e054787.
2025-12-09 20:26:55 +01:00
c45e054787 images fixing with database saves
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 20:12:08 +01:00
8eb2be8628 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:42:28 +00:00
3aafda5f70 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:35:29 +00:00
f27e9a0027 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:27:38 +00:00
921d2527e0 Merge remote-tracking branch 'origin/main'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:25:24 +01:00
901223fcd9 events and gallery backend fix 2025-12-09 19:25:17 +01:00
357d5ba077 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:22:09 +00:00
5d0c0a0b17 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:15:34 +00:00
cb483d8715 picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:08:55 +01:00
3fd5dcf6dd picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:05:37 +01:00
86c2e4e306 picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:02:06 +01:00
0f16b944bc picture test 2025-12-09 19:02:06 +01:00
10752a7337 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:57:06 +00:00
901b6a11db Update events 2025-12-09 17:54:50 +00:00
10192e2627 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:37:30 +01:00
b1d2f8b441 feat: Add vips dependencies for sharp in backend Dockerfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:35:57 +01:00
5215765588 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:31:37 +00:00
c368be5a27 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:27:33 +00:00
54de8e36e2 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:25:00 +00:00
57d7d48d5d Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:16:54 +00:00
b2f04dc726 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:13:20 +00:00
6ea6a58532 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:10:25 +01:00
f00a2ef934 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:06:26 +01:00
ccc5c028ba feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:00:32 +01:00
7c96a15c2e feat: Add vips dependencies for sharp in backend Dockerfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation of native modules during build.
2025-12-09 17:58:21 +01:00
7bfb777a74 feat: Add gallery management and dynamic API-based data loading
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced a gallery management section in `admin.astro` for uploading, listing, and deleting gallery images.
- Added dynamic fetching of events and gallery images from the backend in `index.astro`.
- Updated authentication to handle gallery-related UI visibility and actions.
2025-12-09 17:42:27 +01:00
9c3b4be79d Add event image upload endpoint and refactor image upload handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced `/events/upload` endpoint for securely uploading and processing event images.
- Added image validation, resizing, and conversion to WebP with fallback support for original formats.
- Updated `uploadImage` to `uploadEventImage` and introduced `uploadGalleryImage` in `admin.astro`.
2025-12-09 17:34:06 +01:00
89640a3372 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 16:29:43 +00:00
4ed0016be9 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 15:55:21 +00:00
745888d01b Merge remote-tracking branch 'origin/main'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 16:51:32 +01:00
febd5a886c feat: Add production migration script for Fly.io deployment
- Create standalone migration script that works in production
- Include migration script and images in Docker build
- Images will be copied to /app/data/images on container start
- Can be run with: node migrate-production.js
2025-12-09 16:50:48 +01:00
25305c4aad Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-09 15:50:36 +00: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
af4877300f Add public endpoints and refactor deployments
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Implemented public `/gallery/public` and `/events/public` endpoints for fetching published data without authentication.
- Updated persistent volume configuration for Fly.io across backend and static file serving.
- Adjusted frontend to dynamically fetch events and gallery images from backend API.
- Refined Woodpecker pipeline for clearer separation of backend and frontend deployments.
2025-12-09 15:53:39 +01:00
75 changed files with 4636 additions and 518 deletions

View File

@ -1,4 +1,85 @@
steps: steps:
audit_dependencies:
image: node:20
commands:
- npm install --package-lock-only
- npm audit --audit-level=moderate --json > audit-result.json 2>&1 || echo "Audit completed"
- npm audit --audit-level=moderate > audit-output.txt 2>&1 || echo "Audit completed"
when:
- branch: main
event: push
discord_notify_audit:
image: alpine:latest
environment:
DISCORD_WEBHOOK:
from_secret: discord_webhook
commands:
- apk add --no-cache curl jq
- |
if [ -f audit-result.json ]; then
TOTAL=$(jq -r '.metadata.vulnerabilities.total // 0' audit-result.json 2>/dev/null || echo "0")
CRITICAL=$(jq -r '.metadata.vulnerabilities.critical // 0' audit-result.json 2>/dev/null || echo "0")
HIGH=$(jq -r '.metadata.vulnerabilities.high // 0' audit-result.json 2>/dev/null || echo "0")
MODERATE=$(jq -r '.metadata.vulnerabilities.moderate // 0' audit-result.json 2>/dev/null || echo "0")
LOW=$(jq -r '.metadata.vulnerabilities.low // 0' audit-result.json 2>/dev/null || echo "0")
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ] || [ "$MODERATE" -gt 0 ]; then
COLOR=16744448
STATUS="⚠️ Vulnerabilities Found"
else
COLOR=3066993
STATUS="✅ No Vulnerabilities"
fi
if [ -f audit-output.txt ]; then
VULNS=$(head -50 audit-output.txt | tail -40 || echo "No details")
else
VULNS="No audit output available"
fi
printf '%s' "$VULNS" > /tmp/vulns.txt
PAYLOAD=$(jq -n \
--arg title "🔒 Security Audit - Build #${CI_BUILD_NUMBER}" \
--arg status "$STATUS" \
--arg total "$TOTAL" \
--arg critical "$CRITICAL" \
--arg high "$HIGH" \
--arg moderate "$MODERATE" \
--arg low "$LOW" \
--arg commit "${CI_COMMIT_SHA:0:7}" \
--rawfile details /tmp/vulns.txt \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
--argjson color "$COLOR" \
'{
embeds: [{
title: $title,
description: $status,
color: $color,
fields: [
{ name: "Total", value: $total, inline: true },
{ name: "Critical", value: $critical, inline: true },
{ name: "High", value: $high, inline: true },
{ name: "Moderate", value: $moderate, inline: true },
{ name: "Low", value: $low, inline: true },
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
{ name: "Details", value: ("```\n" + ($details[:800]) + (if ($details | length) > 800 then "\n... (truncated)" else "" end) + "\n```"), inline: false }
],
timestamp: $timestamp
}]
}')
curl -H "Content-Type: application/json" -X POST \
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
else
echo "No audit results found - listing workspace files:"
ls -la
fi
when:
- branch: main
event: push
deploy_frontend: deploy_frontend:
image: node:20 image: node:20
environment: environment:
@ -8,20 +89,88 @@ steps:
- curl -L https://fly.io/install.sh | sh - curl -L https://fly.io/install.sh | sh
- export PATH="$HOME/.fly/bin:$PATH" - export PATH="$HOME/.fly/bin:$PATH"
- flyctl deploy --config fly.toml --app gallus-pub --remote-only - flyctl deploy --config fly.toml --app gallus-pub --remote-only
build_and_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: when:
branch: - branch: main
- main event: push
event:
- push notify_success:
image: alpine:latest
environment:
DISCORD_WEBHOOK:
from_secret: discord_webhook
commands:
- apk add --no-cache curl jq
- |
# Schreibe Commit-Message in Datei (sicher gegen Shell-Sonderzeichen)
printf '%s\n' "$CI_COMMIT_MESSAGE" > /tmp/commit_msg.txt
PAYLOAD=$(cat /tmp/commit_msg.txt | jq -Rs \
--arg title "✅ Build #${CI_BUILD_NUMBER} - Success" \
--arg repo "${CI_REPO}" \
--arg branch "${CI_COMMIT_BRANCH}" \
--arg commit "${CI_COMMIT_SHA:0:7}" \
--arg author "${CI_COMMIT_AUTHOR}" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
'. as $message | {
embeds: [{
title: $title,
description: "Build und Deployment erfolgreich abgeschlossen!",
color: 3066993,
fields: [
{ name: "Repository", value: $repo, inline: true },
{ name: "Branch", value: $branch, inline: true },
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
{ name: "Author", value: $author, inline: true },
{ name: "Commit Message", value: $message, inline: false }
],
timestamp: $timestamp
}]
}')
curl -H "Content-Type: application/json" -X POST \
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
when:
- branch: main
event: push
status: success
notify_failure:
image: alpine:latest
environment:
DISCORD_WEBHOOK:
from_secret: discord_webhook
commands:
- apk add --no-cache curl jq
- |
# Schreibe Commit-Message in Datei (sicher gegen Shell-Sonderzeichen)
printf '%s\n' "$CI_COMMIT_MESSAGE" > /tmp/commit_msg.txt
PAYLOAD=$(cat /tmp/commit_msg.txt | jq -Rs \
--arg title "❌ Build #${CI_BUILD_NUMBER} - Failure" \
--arg repo "${CI_REPO}" \
--arg branch "${CI_COMMIT_BRANCH}" \
--arg commit "${CI_COMMIT_SHA:0:7}" \
--arg author "${CI_COMMIT_AUTHOR}" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
'. as $message | {
embeds: [{
title: $title,
description: "Build oder Deployment ist fehlgeschlagen!",
color: 15158332,
fields: [
{ name: "Repository", value: $repo, inline: true },
{ name: "Branch", value: $branch, inline: true },
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
{ name: "Author", value: $author, inline: true },
{ name: "Commit Message", value: $message, inline: false }
],
timestamp: $timestamp
}]
}')
curl -H "Content-Type: application/json" -X POST \
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
when:
- branch: main
event: push
status: failure

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? ## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 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

10
backend/.gitignore vendored
View File

@ -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

View File

@ -5,8 +5,8 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Install build dependencies for native modules (better-sqlite3) # Install build dependencies for native modules (better-sqlite3, sharp)
RUN apk add --no-cache python3 make g++ RUN apk add --no-cache python3 make g++ vips-dev
# Install dependencies # Install dependencies
COPY package*.json ./ COPY package*.json ./
@ -24,16 +24,28 @@ FROM node:20-alpine
WORKDIR /app WORKDIR /app
# Install runtime dependencies (git for simple-git, sqlite3 CLI tool) # Install runtime dependencies (git for simple-git, sqlite3 CLI tool, vips for sharp)
RUN apk add --no-cache git sqlite # Note: python3, make, g++ are needed for native module compilation
RUN apk add --no-cache git sqlite vips vips-dev python3 make g++
# Copy production dependencies from builder (already compiled native modules) # Copy package files first
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package*.json ./
# Install all production dependencies and rebuild sharp for linuxmusl-x64
RUN npm ci --omit=dev || npm install --production && \
npm rebuild sharp
# Clean up build dependencies after installation to reduce image size
RUN apk del python3 make g++ vips-dev
# Copy built files from builder # Copy built files from builder
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/src/db/migrations ./dist/db/migrations COPY --from=builder /app/src/db/migrations ./dist/db/migrations
# Copy migration script and migrated images
COPY --from=builder /app/migrate-production.js ./migrate-production.js
COPY --from=builder /app/data/images ./data/images
# Create directories # Create directories
RUN mkdir -p /app/workspace /app/data RUN mkdir -p /app/workspace /app/data
@ -56,4 +68,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" CMD node -e "require('http').get('http://localhost:8080/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run DB migrations if present, then start application # Run DB migrations if present, then start application
CMD ["/bin/sh", "-lc", "[ -f dist/migrate.js ] && node dist/migrate.js || true; node dist/index.js"] CMD ["/bin/sh", "-lc", "mkdir -p /app/data/images/events /app/data/images/gallery && [ -f dist/migrate.js ] && node dist/migrate.js || true; node dist/index.js"]

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

@ -14,7 +14,7 @@ primary_region = "ams"
GIT_WORKSPACE_DIR = "/app/data/workspace" GIT_WORKSPACE_DIR = "/app/data/workspace"
# Cross-site frontend and OAuth # Cross-site frontend and OAuth
FRONTEND_URL = "https://gallus-pub.ch" FRONTEND_URL = "https://gallus-pub.ch"
CORS_ORIGIN = "https://gallus-pub.ch" CORS_ORIGIN = "https://gallus-pub.ch,https://www.gallus-pub.ch"
GITEA_REDIRECT_URI = "https://cms.gallus-pub.ch/api/auth/callback" GITEA_REDIRECT_URI = "https://cms.gallus-pub.ch/api/auth/callback"
[http_service] [http_service]

View File

@ -0,0 +1,183 @@
// Production migration script - can be run directly with node
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
// Database schema
const events = sqliteTable('events', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
date: text('date').notNull(),
description: text('description').notNull(),
imageUrl: text('image_url').notNull(),
displayOrder: integer('display_order').notNull(),
isPublished: integer('is_published', { mode: 'boolean' }).default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
});
const galleryImages = sqliteTable('gallery_images', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
imageUrl: text('image_url').notNull(),
altText: text('alt_text').notNull(),
displayOrder: integer('display_order').notNull(),
isPublished: integer('is_published', { mode: 'boolean' }).default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
});
// Old events data
const oldEvents = [
{
title: "Karaoke",
date: "2025-12-31",
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>`,
imageUrl: "/images/events/event_karaoke.webp",
displayOrder: 0,
},
{
title: "Pub Quiz",
date: "2025-12-31",
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,
},
{
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>`,
imageUrl: "/images/events/event_schlager-karaoke.webp",
displayOrder: 2,
},
{
title: "Adventskalender",
date: "2025-12-20",
description: `Jeden Tag neue Überraschungen! Check unsere Social Media Stories!`,
imageUrl: "/images/events/event_advents-kalender.webp",
displayOrder: 3,
},
{
title: "Santa Karaoke-Party",
date: "2025-12-06",
description: `🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻`,
imageUrl: "/images/events/event_santa_karaoke.webp",
displayOrder: 4,
},
{
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`,
imageUrl: "/images/events/event_ferien.webp",
displayOrder: 5,
},
{
title: "Neujahrs-Apero",
date: "2026-01-02",
description: `18:00-20:00 Uhr`,
imageUrl: "/images/events/event_neujahrs-apero.webp",
displayOrder: 6,
},
];
// Old gallery images
const oldGalleryImages = [
{ imageUrl: "/images/gallery/Gallery7.webp", alt: "Gallery 7", order: 0 },
{ imageUrl: "/images/gallery/Gallery8.webp", alt: "Gallery 8", order: 1 },
{ imageUrl: "/images/gallery/Gallery9.webp", alt: "Gallery 9", order: 2 },
{ imageUrl: "/images/gallery/Gallery6.webp", alt: "Gallery 6", order: 3 },
{ imageUrl: "/images/gallery/Gallery1.webp", alt: "Gallery 1", order: 4 },
{ imageUrl: "/images/gallery/Gallery2.webp", alt: "Gallery 2", order: 5 },
{ imageUrl: "/images/gallery/Gallery3.webp", alt: "Gallery 3", order: 6 },
{ imageUrl: "/images/gallery/Gallery4.webp", alt: "Gallery 4", order: 7 },
{ imageUrl: "/images/gallery/Gallery5.webp", alt: "Gallery 5", order: 8 },
];
async function main() {
console.log('=== Production Migration Script ===\n');
const dbPath = process.env.DATABASE_PATH || '/app/data/gallus_cms.db';
console.log('Database path:', dbPath);
// Check if database exists
if (!fs.existsSync(dbPath)) {
console.error('ERROR: Database not found at:', dbPath);
console.error('Please ensure the backend has been started at least once to create the database.');
process.exit(1);
}
// Check if images exist
const dataDir = process.env.GIT_WORKSPACE_DIR || '/app/data';
const eventsDir = path.join(dataDir, 'images', 'events');
const galleryDir = path.join(dataDir, 'images', 'gallery');
console.log('Events images directory:', eventsDir);
console.log('Gallery images directory:', galleryDir);
if (!fs.existsSync(eventsDir)) {
console.error('ERROR: Events images directory not found:', eventsDir);
process.exit(1);
}
if (!fs.existsSync(galleryDir)) {
console.error('ERROR: Gallery images directory not found:', galleryDir);
process.exit(1);
}
// List available images
console.log('\nAvailable event images:', fs.readdirSync(eventsDir));
console.log('Available gallery images:', fs.readdirSync(galleryDir));
// Connect to database
const sqlite = new Database(dbPath);
const db = drizzle(sqlite);
console.log('\n=== Migrating Events ===\n');
for (const event of oldEvents) {
try {
const [newEvent] = await db.insert(events).values({
title: event.title,
date: event.date,
description: event.description,
imageUrl: event.imageUrl,
displayOrder: event.displayOrder,
isPublished: true,
}).returning();
console.log(`✓ Migrated event: ${newEvent.title}`);
} catch (error) {
console.error(`✗ Failed to migrate event "${event.title}":`, error.message);
}
}
console.log('\n=== Migrating Gallery Images ===\n');
for (const img of oldGalleryImages) {
try {
const [newImage] = await db.insert(galleryImages).values({
imageUrl: img.imageUrl,
altText: img.alt,
displayOrder: img.order,
isPublished: true,
}).returning();
console.log(`✓ Migrated gallery image: ${newImage.altText}`);
} catch (error) {
console.error(`✗ Failed to migrate gallery image "${img.alt}":`, error.message);
}
}
sqlite.close();
console.log('\n✓ Migration completed successfully!');
console.log('\nYou can verify the migration by visiting:');
console.log('- Frontend: https://gallus-pub.ch/');
console.log('- Admin: https://gallus-pub.ch/admin');
}
main().catch(error => {
console.error('\n✗ Migration failed:', error);
process.exit(1);
});

2704
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -1,7 +1,6 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
// Users table - stores Gitea user info for audit and access control
export const users = sqliteTable('users', { export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
giteaId: text('gitea_id').notNull().unique(), giteaId: text('gitea_id').notNull().unique(),
@ -60,3 +59,14 @@ export const publishHistory = sqliteTable('publish_history', {
commitMessage: text('commit_message'), commitMessage: text('commit_message'),
publishedAt: integer('published_at', { mode: 'timestamp' }).default(sql`(unixepoch())`), publishedAt: integer('published_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
}); });
// Banner table (for announcements like holidays, special info)
export const banners = sqliteTable('banners', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
text: text('text').notNull(),
startDate: text('start_date').notNull(), // ISO date string
endDate: text('end_date').notNull(), // ISO date string
isActive: integer('is_active', { mode: 'boolean' }).default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
});

View File

@ -16,6 +16,7 @@ import galleryRoute from './routes/gallery.js';
import contentRoute from './routes/content.js'; import contentRoute from './routes/content.js';
import settingsRoute from './routes/settings.js'; import settingsRoute from './routes/settings.js';
import publishRoute from './routes/publish.js'; import publishRoute from './routes/publish.js';
import bannersRoute from './routes/banners.js';
// Validate environment variables // Validate environment variables
try { try {
@ -39,8 +40,24 @@ const fastify = Fastify({
}); });
// Register plugins // Register plugins
// Support multiple origins for CORS
const allowedOrigins = env.CORS_ORIGIN.split(',').map(o => o.trim());
fastify.register(cors, { fastify.register(cors, {
origin: env.CORS_ORIGIN, origin: (origin, cb) => {
// Allow requests with no origin (like mobile apps or curl)
if (!origin) {
return cb(null, true);
}
// Check if origin is in allowed list
const isAllowed = allowedOrigins.includes(origin);
if (isAllowed) {
return cb(null, true);
} else {
return cb(null, false);
}
},
credentials: true, credentials: true,
}); });
@ -60,10 +77,12 @@ fastify.register(multipart, {
}, },
}); });
// Serve static files (uploaded images, etc.) // Serve static files (uploaded images, etc.) from persistent volume
const dataDir = env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
fastify.register(fastifyStatic, { fastify.register(fastifyStatic, {
root: path.join(process.cwd(), 'public'), root: dataDir,
prefix: '/static/', prefix: '/static/',
decorateReply: false
}); });
// Decorate fastify with authenticate method // Decorate fastify with authenticate method
@ -76,6 +95,7 @@ fastify.register(galleryRoute, { prefix: '/api' });
fastify.register(contentRoute, { prefix: '/api' }); fastify.register(contentRoute, { prefix: '/api' });
fastify.register(settingsRoute, { prefix: '/api' }); fastify.register(settingsRoute, { prefix: '/api' });
fastify.register(publishRoute, { prefix: '/api' }); fastify.register(publishRoute, { prefix: '/api' });
fastify.register(bannersRoute, { prefix: '/api' });
// Health check // Health check
fastify.get('/health', async () => { fastify.get('/health', async () => {

View File

@ -6,7 +6,6 @@ import { eq } from 'drizzle-orm';
import { GiteaService } from '../services/gitea.service.js'; import { GiteaService } from '../services/gitea.service.js';
import { env } from '../config/env.js'; import { env } from '../config/env.js';
// Use explicit JSON schema for Fastify route validation to avoid provider issues
const callbackQueryJsonSchema = { const callbackQueryJsonSchema = {
type: 'object', type: 'object',
required: ['code', 'state'], required: ['code', 'state'],

View File

@ -0,0 +1,152 @@
import { FastifyPluginAsync } from 'fastify';
import { db } from '../config/database.js';
import { banners } from '../db/schema.js';
import { eq, and, lte, gte, desc } from 'drizzle-orm';
const bannerBodyJsonSchema = {
type: 'object',
required: ['text', 'startDate', 'endDate'],
properties: {
text: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
isActive: { type: 'boolean' },
},
} as const;
const bannersRoute: FastifyPluginAsync = async (fastify) => {
// Get active banner (public endpoint)
fastify.get('/banners/active', async (request, reply) => {
// Use local date to avoid timezone issues
const now = new Date();
const today = new Date(now.getTime() - (now.getTimezoneOffset() * 60000))
.toISOString()
.split('T')[0]; // YYYY-MM-DD
const [activeBanner] = await db
.select()
.from(banners)
.where(
and(
eq(banners.isActive, true),
lte(banners.startDate, today),
gte(banners.endDate, today)
)
)
.orderBy(desc(banners.createdAt))
.limit(1);
if (!activeBanner) {
return { banner: null };
}
return {
banner: {
id: activeBanner.id,
text: activeBanner.text,
startDate: activeBanner.startDate,
endDate: activeBanner.endDate,
},
};
});
// Get all banners (admin only)
fastify.get('/banners', {
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const allBanners = await db.select().from(banners);
return {
banners: allBanners.map((b: any) => ({
id: b.id,
text: b.text,
startDate: b.startDate,
endDate: b.endDate,
isActive: b.isActive,
createdAt: b.createdAt,
updatedAt: b.updatedAt,
})),
};
});
// Create banner (admin only)
fastify.post('/banners', {
schema: {
body: bannerBodyJsonSchema,
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { text, startDate, endDate, isActive = true } = request.body as any;
const [newBanner] = await db
.insert(banners)
.values({
text,
startDate,
endDate,
isActive,
})
.returning();
return {
banner: {
id: newBanner.id,
text: newBanner.text,
startDate: newBanner.startDate,
endDate: newBanner.endDate,
isActive: newBanner.isActive,
},
};
});
// Update banner (admin only)
fastify.put('/banners/:id', {
schema: {
body: bannerBodyJsonSchema,
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { id } = request.params as { id: string };
const { text, startDate, endDate, isActive } = request.body as any;
const [updated] = await db
.update(banners)
.set({
text,
startDate,
endDate,
isActive,
updatedAt: new Date(),
})
.where(eq(banners.id, id))
.returning();
if (!updated) {
return reply.code(404).send({ error: 'Banner not found' });
}
return {
banner: {
id: updated.id,
text: updated.text,
startDate: updated.startDate,
endDate: updated.endDate,
isActive: updated.isActive,
},
};
});
// Delete banner (admin only)
fastify.delete('/banners/:id', {
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { id } = request.params as { id: string };
await db.delete(banners).where(eq(banners.id, id));
return { success: true };
});
};
export default bannersRoute;

View File

@ -2,6 +2,8 @@ import { FastifyPluginAsync } from 'fastify';
import { db } from '../config/database.js'; import { db } from '../config/database.js';
import { events } from '../db/schema.js'; import { events } from '../db/schema.js';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import fs from 'fs';
import path from 'path';
// Fastify JSON schema for event body // Fastify JSON schema for event body
const eventBodyJsonSchema = { const eventBodyJsonSchema = {
@ -36,7 +38,15 @@ const reorderBodyJsonSchema = {
} as const; } as const;
const eventsRoute: FastifyPluginAsync = async (fastify) => { const eventsRoute: FastifyPluginAsync = async (fastify) => {
// List all events (by displayOrder) // PUBLIC: List published events (no auth required)
fastify.get('/events/public', async () => {
const all = await db.select().from(events)
.where(eq(events.isPublished, true))
.orderBy(events.displayOrder);
return { events: all };
});
// List all events (by displayOrder) - admin only
fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => { fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => {
const all = await db.select().from(events).orderBy(events.displayOrder); const all = await db.select().from(events).orderBy(events.displayOrder);
return { events: all }; return { events: all };
@ -66,6 +76,73 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => {
return { event: row }; return { event: row };
}); });
// Upload event image file (multipart)
fastify.post('/events/upload', {
preHandler: [fastify.authenticate],
}, async (request, reply) => {
try {
// Expect a single file field named "file"
const file = await (request as any).file();
if (!file) {
return reply.code(400).send({ error: 'No file uploaded' });
}
const mime = file.mimetype as string | undefined;
if (!mime || !mime.startsWith('image/')) {
return reply.code(400).send({ error: 'Only image uploads are allowed' });
}
// Prepare directories - use persistent volume for Fly.io
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
const uploadDir = path.join(dataDir, 'public', 'images', 'events');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
// Read uploaded stream into buffer
const chunks: Buffer[] = [];
for await (const chunk of file.file) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const inputBuffer = Buffer.concat(chunks);
// Generate filename
const stamp = Date.now().toString(36);
const rand = Math.random().toString(36).slice(2, 8);
const baseName = `${stamp}-${rand}`;
// Try to convert to webp and limit size; fallback to original
let outBuffer: Buffer | null = null;
let outExt = '.webp';
try {
// Lazy load sharp only when needed
const sharp = (await import('sharp')).default;
outBuffer = await sharp(inputBuffer)
.rotate()
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer();
} catch (err) {
fastify.log.warn({ err }, 'Sharp processing failed, using original image');
outBuffer = inputBuffer;
// naive extension from mimetype
const extFromMime = mime.split('/')[1] || 'bin';
outExt = '.' + extFromMime.replace(/[^a-z0-9]/gi, '').toLowerCase();
}
const filename = baseName + outExt;
const destPath = path.join(uploadDir, filename);
fs.writeFileSync(destPath, outBuffer);
// Public URL (served via /static)
const publicUrl = `/images/events/${filename}`;
return reply.code(201).send({ imageUrl: publicUrl });
} catch (err) {
fastify.log.error({ err }, 'Upload failed');
return reply.code(500).send({ error: 'Failed to upload image' });
}
});
// Delete event // Delete event
fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
const { id } = request.params as { id: string }; const { id } = request.params as { id: string };

View File

@ -5,7 +5,6 @@ import { galleryImages } from '../db/schema.js';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import sharp from 'sharp';
// Fastify JSON schema for gallery image body // Fastify JSON schema for gallery image body
const galleryBodyJsonSchema = { const galleryBodyJsonSchema = {
@ -21,7 +20,15 @@ const galleryBodyJsonSchema = {
const galleryRoute: FastifyPluginAsync = async (fastify) => { const galleryRoute: FastifyPluginAsync = async (fastify) => {
// List all gallery images // PUBLIC: List published gallery images (no auth required)
fastify.get('/gallery/public', async () => {
const images = await db.select().from(galleryImages)
.where(eq(galleryImages.isPublished, true))
.orderBy(galleryImages.displayOrder);
return { images };
});
// List all gallery images - admin only
fastify.get('/gallery', { fastify.get('/gallery', {
preHandler: [fastify.authenticate], preHandler: [fastify.authenticate],
}, async (request, reply) => { }, async (request, reply) => {
@ -77,9 +84,9 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
return reply.code(400).send({ error: 'Only image uploads are allowed' }); return reply.code(400).send({ error: 'Only image uploads are allowed' });
} }
// Prepare directories // Prepare directories - use persistent volume for Fly.io
const publicRoot = path.join(process.cwd(), 'public'); const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
const uploadDir = path.join(publicRoot, 'images', 'gallery'); const uploadDir = path.join(dataDir, 'public', 'images', 'gallery');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
// Read uploaded stream into buffer // Read uploaded stream into buffer
@ -98,12 +105,15 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
let outBuffer: Buffer | null = null; let outBuffer: Buffer | null = null;
let outExt = '.webp'; let outExt = '.webp';
try { try {
// Lazy load sharp only when needed
const sharp = (await import('sharp')).default;
outBuffer = await sharp(inputBuffer) outBuffer = await sharp(inputBuffer)
.rotate() .rotate()
.resize({ width: 1600, withoutEnlargement: true }) .resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 82 }) .webp({ quality: 82 })
.toBuffer(); .toBuffer();
} catch { } catch (err) {
fastify.log.warn({ err }, 'Sharp processing failed, using original image');
outBuffer = inputBuffer; outBuffer = inputBuffer;
// naive extension from mimetype // naive extension from mimetype
const extFromMime = mime.split('/')[1] || 'bin'; const extFromMime = mime.split('/')[1] || 'bin';
@ -115,7 +125,7 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
fs.writeFileSync(destPath, outBuffer); fs.writeFileSync(destPath, outBuffer);
// Public URL (served via /static) // Public URL (served via /static)
const publicUrl = `/static/images/gallery/${filename}`; const publicUrl = `/images/gallery/${filename}`;
// Store in DB (optional but useful) // Store in DB (optional but useful)
const [row] = await db.insert(galleryImages).values({ const [row] = await db.insert(galleryImages).values({

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

@ -43,6 +43,7 @@ export class FileGeneratorService {
return `--- return `---
import Layout from "../components/Layout.astro"; import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
import Hero from "../components/Hero.astro"; import Hero from "../components/Hero.astro";
import Welcome from "../components/Welcome.astro"; import Welcome from "../components/Welcome.astro";
import EventsGrid from "../components/EventsGrid.astro"; import EventsGrid from "../components/EventsGrid.astro";
@ -62,6 +63,7 @@ ${imagesCode}
<Layout> <Layout>
\t<Hero id="hero" /> \t<Hero id="hero" />
\t<Banner />
\t<Welcome id="welcome" /> \t<Welcome id="welcome" />
\t<EventsGrid id="events" events={events} /> \t<EventsGrid id="events" events={events} />
\t<ImageCarousel id="gallery" images={images} /> \t<ImageCarousel id="gallery" images={images} />

1092
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 KiB

View File

@ -0,0 +1,40 @@
---
// src/components/Banner.astro
import "../styles/components/Banner.css"
---
<div id="banner-container"></div>
<script>
const API_BASE = 'https://cms.gallus-pub.ch';
async function loadBanner() {
try {
const response = await fetch(`${API_BASE}/api/banners/active`);
if (response.ok) {
const data = await response.json();
if (data.banner) {
const container = document.getElementById('banner-container');
if (container) {
container.innerHTML = `
<div class="banner-wrapper">
<div class="banner container">
<p>${data.banner.text}</p>
</div>
</div>
`;
}
}
}
} catch (error) {
console.error('Failed to fetch banner:', error);
}
}
// Load banner when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadBanner);
} else {
loadBanner();
}
</script>

View File

@ -1,8 +1,10 @@
--- ---
import Layout from "../components/Layout.astro"; import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
--- ---
<Layout> <Layout>
<Banner />
<h1>Gallery</h1> <h1>Gallery</h1>

View File

@ -1,8 +1,10 @@
--- ---
import Layout from "../components/Layout.astro"; import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
--- ---
<Layout> <Layout>
<Banner />
<h1>Openings</h1> <h1>Openings</h1>

View File

@ -51,7 +51,6 @@ const title = 'Admin';
<label>Datum<input id="ev-date" type="date" placeholder="z. B. 2025-12-31" /></label> <label>Datum<input id="ev-date" type="date" placeholder="z. B. 2025-12-31" /></label>
<label>Beschreibung<textarea id="ev-desc" rows="4"></textarea></label> <label>Beschreibung<textarea id="ev-desc" rows="4"></textarea></label>
<label>Bild-Datei<input id="ev-file" type="file" accept="image/*" /></label> <label>Bild-Datei<input id="ev-file" type="file" accept="image/*" /></label>
<label>Alt-Text<input id="ev-alt" placeholder="Bildbeschreibung" /></label>
<button id="btn-create-ev">Event anlegen</button> <button id="btn-create-ev">Event anlegen</button>
<div id="ev-create-msg" class="muted"></div> <div id="ev-create-msg" class="muted"></div>
</div> </div>
@ -68,6 +67,41 @@ const title = 'Admin';
</div> </div>
</section> </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>Gallery-Liste</h3>
<div id="gallery-list" class="grid"></div>
</div>
</div>
</section>
<section id="sec-banner" style="display:none">
<h2>Banner verwalten</h2>
<div class="events-row">
<div class="card">
<h3>Neuen Banner erstellen</h3>
<label>Text<textarea id="banner-text" rows="3" placeholder="z.B. Wir sind vom 24.12. bis 02.01. geschlossen"></textarea></label>
<label>Von (Datum)<input id="banner-start" type="date" /></label>
<label>Bis (Datum)<input id="banner-end" type="date" /></label>
<button id="btn-create-banner">Banner erstellen</button>
<div id="banner-create-msg" class="muted"></div>
</div>
<div class="card" style="min-width:380px;">
<h3>Banner-Liste</h3>
<div id="banner-list" class="grid"></div>
</div>
</div>
</section>
<section id="sec-publish" style="display:none"> <section id="sec-publish" style="display:none">
<h2>Veröffentlichen</h2> <h2>Veröffentlichen</h2>
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label> <label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
@ -92,15 +126,21 @@ const title = 'Admin';
document.getElementById('auth-status').textContent = `Angemeldet als ${me.user?.giteaUsername || 'Admin'}`; document.getElementById('auth-status').textContent = `Angemeldet als ${me.user?.giteaUsername || 'Admin'}`;
// UI-Bereiche für eingeloggte Nutzer einblenden // UI-Bereiche für eingeloggte Nutzer einblenden
document.getElementById('sec-events').style.display = ''; document.getElementById('sec-events').style.display = '';
document.getElementById('sec-gallery').style.display = '';
document.getElementById('sec-banner').style.display = '';
document.getElementById('sec-publish').style.display = ''; document.getElementById('sec-publish').style.display = '';
// Direkt Events laden und auf Sektion fokussieren // Direkt Events laden und auf Sektion fokussieren
await loadEvents(); await loadEvents();
await loadGallery();
await loadBanners();
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' }); document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
} catch (e) { } catch (e) {
const el = document.getElementById('auth-status'); const el = document.getElementById('auth-status');
el.textContent = 'Nicht angemeldet'; el.textContent = 'Nicht angemeldet';
// Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden. // Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden.
document.getElementById('sec-events').style.display = 'none'; document.getElementById('sec-events').style.display = 'none';
document.getElementById('sec-gallery').style.display = 'none';
document.getElementById('sec-banner').style.display = 'none';
document.getElementById('sec-publish').style.display = 'none'; document.getElementById('sec-publish').style.display = 'none';
} }
} }
@ -125,7 +165,15 @@ const title = 'Admin';
}); });
// ========== Events & Publish ========== // ========== Events & Publish ==========
async function uploadImage(file, altText) { async function uploadEventImage(file) {
const fd = new FormData();
fd.append('file', file);
const res = await fetch(API_BASE + '/api/events/upload', { method: 'POST', body: fd, credentials: 'include' });
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function uploadGalleryImage(file, altText) {
const fd = new FormData(); const fd = new FormData();
fd.append('file', file); fd.append('file', file);
if (altText) fd.append('altText', altText); if (altText) fd.append('altText', altText);
@ -175,7 +223,7 @@ const title = 'Admin';
</div> </div>
<div class="muted">${ev.date}</div> <div class="muted">${ev.date}</div>
<div>${ev.description || ''}</div> <div>${ev.description || ''}</div>
<div class="muted">Bild: ${ev.imageUrl || ''}</div> ${ev.imageUrl ? `<img src="${API_BASE}${ev.imageUrl}" alt="${ev.title}" class="thumb" style="margin-top:0.5rem;" />` : '<div class="muted">Kein Bild</div>'}
<div class="row-buttons"> <div class="row-buttons">
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button> <button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
</div>`; </div>`;
@ -230,14 +278,13 @@ const title = 'Admin';
const date = (document.getElementById('ev-date')).value.trim(); const date = (document.getElementById('ev-date')).value.trim();
const desc = (document.getElementById('ev-desc')).value.trim(); const desc = (document.getElementById('ev-desc')).value.trim();
const file = /** @type {HTMLInputElement} */ (document.getElementById('ev-file')).files[0]; const file = /** @type {HTMLInputElement} */ (document.getElementById('ev-file')).files[0];
const alt = (document.getElementById('ev-alt')).value.trim();
const msg = document.getElementById('ev-create-msg'); const msg = document.getElementById('ev-create-msg');
msg.textContent = 'Lade Bild hoch...'; msg.textContent = 'Lade Bild hoch...';
try { try {
let imageUrl = ''; let imageUrl = '';
if (file) { if (file) {
const up = await uploadImage(file, alt || title); const up = await uploadEventImage(file);
imageUrl = up?.image?.imageUrl || ''; imageUrl = up?.imageUrl || '';
} }
msg.textContent = 'Lege Event an...'; msg.textContent = 'Lege Event an...';
await api('/api/events', { await api('/api/events', {
@ -249,7 +296,6 @@ const title = 'Admin';
(document.getElementById('ev-date')).value = ''; (document.getElementById('ev-date')).value = '';
(document.getElementById('ev-desc')).value = ''; (document.getElementById('ev-desc')).value = '';
(document.getElementById('ev-file')).value = ''; (document.getElementById('ev-file')).value = '';
(document.getElementById('ev-alt')).value = '';
await loadEvents(); await loadEvents();
} catch(e){ msg.textContent = 'Fehler: '+e.message } } catch(e){ msg.textContent = 'Fehler: '+e.message }
}); });
@ -288,6 +334,147 @@ const title = 'Admin';
} catch(e){ msg.textContent = 'Fehler: '+e.message } } catch(e){ msg.textContent = 'Fehler: '+e.message }
}); });
// ========== Gallery ==========
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 = '';
const galleryImages = (data.images || []).slice();
galleryImages.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
galleryImages.forEach((img) => {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<img src="${API_BASE}${img.imageUrl}" alt="${img.altText}" class="thumb" />
<div class="muted">${img.altText || ''}</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); }
})
})
} catch (e) {
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
console.error(e);
}
}
document.getElementById('btn-create-gal').addEventListener('click', async () => {
const file = /** @type {HTMLInputElement} */ (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 Datei auswählen';
return;
}
msg.textContent = 'Lade Bild hoch...';
try {
await uploadGalleryImage(file, alt);
msg.textContent = 'Bild hochgeladen';
(document.getElementById('gal-file')).value = '';
(document.getElementById('gal-alt')).value = '';
await loadGallery();
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
// ========== Banner ==========
async function loadBanners() {
const listEl = document.getElementById('banner-list');
listEl.innerHTML = '<div class="muted">Lade...</div>';
try {
const data = await api('/api/banners');
listEl.innerHTML = '';
const bannersList = (data.banners || []).slice();
bannersList.sort((a,b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
bannersList.forEach((banner) => {
const card = document.createElement('div');
card.className = 'card';
const statusText = banner.isActive ? '✓ Aktiv' : '✗ Inaktiv';
card.innerHTML = `
<div><strong>${banner.text.substring(0, 60)}${banner.text.length > 60 ? '...' : ''}</strong></div>
<div class="muted">Von: ${banner.startDate}</div>
<div class="muted">Bis: ${banner.endDate}</div>
<div class="pill">${statusText}</div>
<div class="row-buttons">
<button data-id="${banner.id}" class="btn-toggle-banner">${banner.isActive ? 'Deaktivieren' : 'Aktivieren'}</button>
<button data-id="${banner.id}" class="btn-del-banner">Löschen</button>
</div>`;
listEl.appendChild(card);
});
listEl.querySelectorAll('.btn-toggle-banner').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
const banner = bannersList.find(b => b.id === id);
if (!banner) return;
try {
await api(`/api/banners/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: banner.text,
startDate: banner.startDate,
endDate: banner.endDate,
isActive: !banner.isActive
})
});
await loadBanners();
} catch(e){ alert('Fehler: '+e.message); }
})
});
listEl.querySelectorAll('.btn-del-banner').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
if (!confirm('Banner wirklich löschen?')) return;
try { await api(`/api/banners/${id}`, { method: 'DELETE' }); await loadBanners(); } catch(e){ alert('Fehler: '+e.message); }
})
})
} catch (e) {
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
console.error(e);
}
}
document.getElementById('btn-create-banner').addEventListener('click', async () => {
const text = (document.getElementById('banner-text')).value.trim();
const startDate = (document.getElementById('banner-start')).value.trim();
const endDate = (document.getElementById('banner-end')).value.trim();
const msg = document.getElementById('banner-create-msg');
if (!text || !startDate || !endDate) {
msg.textContent = 'Bitte alle Felder ausfüllen';
return;
}
msg.textContent = 'Erstelle Banner...';
try {
await api('/api/banners', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, startDate, endDate, isActive: true })
});
msg.textContent = 'Banner erstellt';
(document.getElementById('banner-text')).value = '';
(document.getElementById('banner-start')).value = '';
(document.getElementById('banner-end')).value = '';
await loadBanners();
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
refreshAuth(); refreshAuth();
</script> </script>
</body> </body>

View File

@ -1,5 +1,6 @@
--- ---
import Layout from "../components/Layout.astro"; import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
import Hero from "../components/Hero.astro"; import Hero from "../components/Hero.astro";
import Welcome from "../components/Welcome.astro"; import Welcome from "../components/Welcome.astro";
import EventsGrid from "../components/EventsGrid.astro"; import EventsGrid from "../components/EventsGrid.astro";
@ -10,22 +11,68 @@ import About from "../components/About.astro";
const events = [ const events = [
{ {
image: "/static/images/gallery/miyma9zc-8he1di.webp", image: "/images/events/mj7dj1ko-mtnbg6.jpeg",
title: "Test", title: "Karaoke",
date: "2025-12-10", date: "2025-12-01",
description: ` description: `
Das ist ein test event Von Mittwoch bis Samstag kannst du deine Stimme zum Besten geben. Du singst gerne, aber lieber für dich? Dann kannst du den 2. OG auch privat mieten. 🍀 WA 077 232 27 70
`,
},
{
image: "/images/events/mj67800i-6ng82x.jpeg",
title: "Pub Quiz",
date: "2025-12-02",
description: `
Jeden Freitag 20:00Uhr-ca 21:30Uhr.
Plätze sind begrenzt! Jetzt reservieren unter 🍀WA 077 232 27 70
`,
},
{
image: "/images/events/mjbgxwyk-ygcymt.jpeg",
title: "Schlager Flyer",
date: "2026-01-15",
description: `
Schalger- HüttenzauberKARAOKE geht in die 2.Runde!
Eintritt ist frei!
Plätze reservieren unter WA 077 232 27 70
`,
},
{
image: "/images/events/mj7donky-md8jp5.jpeg",
title: "Celtik Folk Night",
date: "2026-01-29",
description: `
Celtic Folk Night im Gallus Pub!✨🌿20:30Uhr Eintritt ist Frei/Hutkollekte. Reservation via WA 077 232 27 70
`,
},
{
image: "/images/events/mk6wdnz2-rpxzvl.jpeg",
title: "Pg Petricca - LIVE",
date: "2026-03-20",
description: `
LIVE Musik mit Pg Petricca! - Folk & Blues.
Eintritt ist Frei / Hutkollekte
Reservation unter 🍀WA 077 232 27 70
`, `,
} }
]; ];
const images = [ const images = [
{ src: "/static/images/gallery/miyma9zc-8he1di.webp", alt: "Schwarzes bild" } { src: "/images/gallery/miywxkwh-m4xaww.webp", alt: "miywxkwh-m4xaww.webp" },
{ src: "/images/gallery/miyxgbqr-n3zzrg.png", alt: "miyxgbqr-n3zzrg.png" },
{ src: "/images/gallery/miyxgfh1-c7zawh.png", alt: "miyxgfh1-c7zawh.png" },
{ src: "/images/gallery/miyxgjff-wjtyim.png", alt: "miyxgjff-wjtyim.png" },
{ src: "/images/gallery/miyxgn6h-jsaltu.png", alt: "miyxgn6h-jsaltu.png" },
{ src: "/images/gallery/mj67l5x3-pdasw8.jpeg", alt: "mj67l5x3-pdasw8.jpeg" },
{ src: "/images/gallery/mj67mw2z-3pd81q.jpeg", alt: "mj67mw2z-3pd81q.jpeg" },
{ src: "/images/gallery/mj67nwjs-6oaijj.jpeg", alt: "mj67nwjs-6oaijj.jpeg" },
{ src: "/images/gallery/mj67ove6-el3pf7.png", alt: "mj67ove6-el3pf7.png" }
]; ];
--- ---
<Layout> <Layout>
<Hero id="hero" /> <Hero id="hero" />
<Banner />
<Welcome id="welcome" /> <Welcome id="welcome" />
<EventsGrid id="events" events={events} /> <EventsGrid id="events" events={events} />
<ImageCarousel id="gallery" images={images} /> <ImageCarousel id="gallery" images={images} />

View File

@ -0,0 +1,26 @@
.banner-wrapper {
width: 100%;
background-color: var(--color-orange1);
padding: 1rem 0;
}
.banner {
max-width: var(--container-max-width);
margin: 0 auto;
padding: 0 var(--padding-horizontal);
}
.banner p {
color: #000;
font-size: var(--font-size-small-medium);
font-weight: 600;
margin: 0;
text-align: center;
line-height: 1.4;
}
@media (max-width: 768px) {
.banner p {
font-size: var(--font-size-small);
}
}