- HTML 55%
- TypeScript 21.5%
- Go 14.1%
- CSS 5.8%
- JavaScript 3.2%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Remove line-clamp CSS restrictions from .timetable-band-content to allow text to wrap freely instead of being capped at 2 lines - Measure actual content height after rendering and apply dynamic block height (max of time-duration height, measured content height, 20px minimum) so blocks automatically grow taller when they need more space for names/ locations instead of truncating content Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| backend | ||
| config | ||
| frontend | ||
| graphify-out | ||
| migration | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .woodpecker.yaml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| sboa26-signup-firebase.html | ||
Festival Track - Live Signup Board
Festival Track is a live festival signup board that replaces the legacy Firebase-based HTML file. It allows festival participants to sign up for performances in real time, with live synchronization across all connected browsers—no page reload necessary.
Prerequisites
- Docker (version 20.10+)
- Docker Compose (version 2.0+)
- A text editor to configure
config/festival.ini
Quick Start
-
Prepare environment variables:
cp .env.example .envEdit
.envand set a strong password forPOSTGRES_PASSWORD. Never commit this file to version control. -
Configure the festival lineup:
cp config/festival.ini.example config/festival.iniconfig/festival.iniis gitignored (just like.env) so your real lineup and participant names never end up in version control -- only the generic.exampletemplate is tracked. Editconfig/festival.inito set:- Event metadata (
[event]section): title, tagline, description, footer - Participant names (
[participants]section): comma-separated list of names - Days and stages:
[days]section lists the day order; one[<DayName>]section per day withstages=...line; one[<DayName>.<StageName>]section per stage withBand Name = HH:MMentries (orBand Name = HH:MM-HH:MMfor optional end times)
- Event metadata (
-
Copy the app config:
cp config/app.ini.example config/app.iniconfig/app.iniis gitignored (likefestival.iniand.env) so local debug settings never end up in version control. Editconfig/app.iniif you need app-wide or debug settings (currently used for time overrides during testing). -
Start the services:
docker compose up -d --buildThis builds the Docker image and starts both the PostgreSQL database and the web app.
-
Access the app: Open your browser to
http://localhost:8080(or the port you set in.envviaAPP_PORT).
Configuration
Environment Variables (.env)
POSTGRES_USER: Database user (default:festival)POSTGRES_PASSWORD: Required. Choose a strong, unique password.POSTGRES_DB: Database name (default:festival)APP_PORT: Host port for the web app (default:8080)DEBUG: Set totrueto dump the fully parsed config (all days, stages, bands, and participants, each with the generated id) todocker compose logs appat startup. Default: off. Useful for verifyingfestival.iniwas parsed as expected.
Config validation
At startup the app validates both festival.ini and app.ini and refuses to start if validation fails.
For festival.ini, the app checks: no participants, duplicate participant names, no days, a day with no stages, a stage with no bands, a band with an empty name/time, a band time that isn't HH:MM, a band end time (if present) that isn't HH:MM, a day with a missing/malformed date, or two bands whose generated ids collide. A sparse lineup (e.g. a day with only one stage) is valid and will not be rejected.
For app.ini, the app checks: if debug is enabled, the now field must be present and match the format YYYY-MM-DDTHH:MM:SS; and, if set, [theme] variant must be classic or modern.
INI Configuration (config/festival.ini)
Example structure:
[event]
title = Festival Name
eyebrow = Location / Year
subtitle = Description for participants
footer = Footer text
[participants]
names = Alice, Bob, Charlie, Diana
[days]
order = Day 1, Day 2, Day 3
[Day 1]
date = 2026-08-13
stages = Main Stage, Side Stage
[Day 1.Main Stage]
Band A = 14:00
Band B = 15:30
Band C = 17:00
[Day 1.Side Stage]
Band X = 14:30
Band Y = 16:00
Band Z = 17:00-17:45
Every day section needs a date = YYYY-MM-DD -- the real calendar date that day starts on. This is what lets the "Als Nächstes" section on the page work out what's genuinely coming up next (across the whole event, not just the currently selected day tab), instead of guessing from the system clock and whichever day happens to be selected. A band timed before 06:00 (e.g. 01:00) is treated as the tail end of that day's program, past midnight into the following real date -- you don't need to adjust anything for that, just set each day's date to the date its program actually starts.
Bands can optionally have an end time: use the format Band Name = HH:MM-HH:MM to specify both start and end times. If only a start time is needed, use Band Name = HH:MM as usual. Bands with an end time show up in the "Jetzt läuft" section while they're running (see below); bands without one are unaffected and behave exactly as before.
Optionally, festival.ini can also define work-shift areas so participants can sign up to help out (bar, setup, etc.) via the /shiftplan.html page -- live-synced the same way as band signups. This is entirely optional: omit the [shifts] section and the shift-planning page simply has nothing to show.
[shifts]
areas = Bar, Setup
[shift.Bar]
locations = Main Counter, Chill Counter
[shift.Setup]
areas = ... in [shifts] lists the areas in display order (same idea as stages = ... for a day). Each [shift.<AreaName>] section is optional; if present, its locations = ... line lists specific places within that area someone could work (e.g. two different bar counters). If an area has no locations line, there's no location picker for it -- just the one implicit place. On the /shiftplan.html page, each person picks a day, area, optional location, and a start/end hour (always full hours) for their own shift; everyone sees everyone else's shifts in a graphical timetable, filterable to one person or shown all at once.
Important: Configuration changes require restarting the app service:
docker compose restart app
App Configuration (config/app.ini)
This file contains app-wide settings (theme, debug). It has the same structure as festival.ini and app.ini.example -- it's gitignored so local settings stay local.
Currently, config/app.ini supports:
[debug]
enabled = false
now = 2026-08-13T15:00:00
[theme]
variant = classic
[debug] enabled: When set totrue, the frontend treats thenowtimestamp as the fixed current time instead of the real system clock. Useful for testing time-dependent features (e.g. "currently playing", "coming up next") without waiting for the real event.[debug] now: The fixed timestamp to use when debug is enabled. Format:YYYY-MM-DDTHH:MM:SS. This field is required ifenabled = true.
While debug mode is active, a banner is shown at the top of the page ("DEBUG-MODUS AKTIV") displaying the fixed timestamp currently in effect, so it's always obvious the page isn't showing real time.
[theme] variant:classic(default) is the original fixed dark look -- no light/dark switch is shown.modernis a newer look that adds a light/dark toggle button at the top of every page; the chosen light/dark mode is remembered per browser (defaulting to the system's own light/dark preference on first visit). Which variant is active is a deploy-time choice made here, not something an individual visitor can change -- only light vs. dark withinmodernis a per-browser runtime choice.
When you change app.ini, restart the app service for changes to take effect:
docker compose restart app
"Jetzt läuft" (currently playing)
After selecting a person, the page shows a "Jetzt läuft" section above "Als Nächstes" listing any bands with an end time that person is signed up for and that are running right now (start time <= now < end time). Multiple bands can be shown at once if overlapping performances are both currently running. The section is hidden entirely when nothing is currently playing. Bands without an end time never appear here, since there's no defined time window to check against.
Architecture
- Frontend: TypeScript single-page application (SPA) built with esbuild. Assets are statically embedded in the Go binary.
- Backend: Go 1.25 REST + WebSocket server that handles:
- Config queries (
/api/config) - Health checks (
/healthz) - Live WebSocket broadcasts for signup changes (
/ws) - Static file serving (frontend assets)
- Config queries (
- Database: PostgreSQL 16 running in a separate container with persistent data volume.
- Networking: Compose-internal network; database is not exposed to the host. Only the web app port (
APP_PORT) is published.
Deployment Notes
TLS/HTTPS
This deployment runs plain HTTP. For production or public-facing use, deploy behind a reverse proxy (Caddy, nginx, Traefik) to provide HTTPS termination. Example with Caddy:
caddy reverse-proxy --from :443 --to localhost:8080
Stopping and Cleanup
Stop services without removing data:
docker compose down
Remove everything including the database volume:
docker compose down -v
Logs
View app logs:
docker compose logs -f app
View database logs:
docker compose logs -f db
Development
To rebuild the image after code changes:
docker compose up -d --build
To run without daemon mode (see logs in real time):
docker compose up --build
Press Ctrl+C to stop.