56 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
52 changed files with 1217 additions and 480 deletions

View File

@ -1,4 +1,85 @@
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:
image: node:20
environment:
@ -9,5 +90,87 @@ steps:
- export PATH="$HOME/.fly/bin:$PATH"
- flyctl deploy --config fly.toml --app gallus-pub --remote-only
when:
branch: main
event: push
- branch: main
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

View File

@ -14,7 +14,7 @@ primary_region = "ams"
GIT_WORKSPACE_DIR = "/app/data/workspace"
# Cross-site frontend and OAuth
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"
[http_service]

View File

@ -1,7 +1,6 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
// Users table - stores Gitea user info for audit and access control
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
giteaId: text('gitea_id').notNull().unique(),
@ -60,3 +59,14 @@ export const publishHistory = sqliteTable('publish_history', {
commitMessage: text('commit_message'),
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 settingsRoute from './routes/settings.js';
import publishRoute from './routes/publish.js';
import bannersRoute from './routes/banners.js';
// Validate environment variables
try {
@ -39,8 +40,24 @@ const fastify = Fastify({
});
// Register plugins
// Support multiple origins for CORS
const allowedOrigins = env.CORS_ORIGIN.split(',').map(o => o.trim());
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,
});
@ -78,6 +95,7 @@ fastify.register(galleryRoute, { prefix: '/api' });
fastify.register(contentRoute, { prefix: '/api' });
fastify.register(settingsRoute, { prefix: '/api' });
fastify.register(publishRoute, { prefix: '/api' });
fastify.register(bannersRoute, { prefix: '/api' });
// Health check
fastify.get('/health', async () => {

View File

@ -6,7 +6,6 @@ import { eq } from 'drizzle-orm';
import { GiteaService } from '../services/gitea.service.js';
import { env } from '../config/env.js';
// Use explicit JSON schema for Fastify route validation to avoid provider issues
const callbackQueryJsonSchema = {
type: 'object',
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

@ -94,7 +94,7 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => {
// 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, 'images', 'events');
const uploadDir = path.join(dataDir, 'public', 'images', 'events');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
// Read uploaded stream into buffer
@ -133,7 +133,7 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => {
fs.writeFileSync(destPath, outBuffer);
// Public URL (served via /static)
const publicUrl = `/static/images/events/${filename}`;
const publicUrl = `/images/events/${filename}`;
return reply.code(201).send({ imageUrl: publicUrl });

View File

@ -86,7 +86,7 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
// 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, 'images', 'gallery');
const uploadDir = path.join(dataDir, 'public', 'images', 'gallery');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
// Read uploaded stream into buffer
@ -125,7 +125,7 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
fs.writeFileSync(destPath, outBuffer);
// Public URL (served via /static)
const publicUrl = `/static/images/gallery/${filename}`;
const publicUrl = `/images/gallery/${filename}`;
// Store in DB (optional but useful)
const [row] = await db.insert(galleryImages).values({

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

1092
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 46 KiB

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

View File

Before

Width:  |  Height:  |  Size: 523 KiB

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

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

Before

Width:  |  Height:  |  Size: 567 KiB

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,9 +1,11 @@
---
import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
---
<Layout>
<Banner />
<h1>Gallery</h1>
<p>Hier findest du alle aktuellen und kommenden Gallery im Gallus Pub.</p>

View File

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

View File

@ -84,6 +84,24 @@ const title = 'Admin';
</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">
<h2>Veröffentlichen</h2>
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
@ -109,10 +127,12 @@ const title = 'Admin';
// UI-Bereiche für eingeloggte Nutzer einblenden
document.getElementById('sec-events').style.display = '';
document.getElementById('sec-gallery').style.display = '';
document.getElementById('sec-banner').style.display = '';
document.getElementById('sec-publish').style.display = '';
// Direkt Events laden und auf Sektion fokussieren
await loadEvents();
await loadGallery();
await loadBanners();
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
} catch (e) {
const el = document.getElementById('auth-status');
@ -120,6 +140,7 @@ const title = 'Admin';
// Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden.
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';
}
}
@ -202,7 +223,7 @@ const title = 'Admin';
</div>
<div class="muted">${ev.date}</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">
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
</div>`;
@ -366,6 +387,94 @@ const title = 'Admin';
} 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();
</script>
</body>

View File

@ -1,5 +1,6 @@
---
import Layout from "../components/Layout.astro";
import Banner from "../components/Banner.astro";
import Hero from "../components/Hero.astro";
import Welcome from "../components/Welcome.astro";
import EventsGrid from "../components/EventsGrid.astro";
@ -9,16 +10,69 @@ import Contact from "../components/Contact.astro";
import About from "../components/About.astro";
const events = [
{
image: "/images/events/mj7dj1ko-mtnbg6.jpeg",
title: "Karaoke",
date: "2025-12-01",
description: `
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 = [
{ src: "/static/images/gallery/miyvtrjn-zoq4j5.webp", alt: "miyvtrjn-zoq4j5.webp" }
{ 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>
<Hero id="hero" />
<Banner />
<Welcome id="welcome" />
<EventsGrid id="events" events={events} />
<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);
}
}