- TypeScript 92.3%
- JavaScript 3.6%
- Dockerfile 2.7%
- Makefile 1.3%
| .forgejo/workflows | ||
| demo | ||
| deploy/podman | ||
| public | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| .prettierignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CLAUDE.md | ||
| config.demo.toml | ||
| config.toml.example | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| LICENSE | ||
| Makefile | ||
| next.config.js | ||
| package.json | ||
| pnpm-lock.yaml | ||
| postcss.config.cjs | ||
| prettier.config.js | ||
| README.md | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Event Display
An app to display events from a nextcloud calendar in the entry hall of the Stadtteilzentrum Nordstadt e.V. in Hannover.
Development
Prerequisites
- Node.js 20.9 or higher (24 LTS recommended, see
.nvmrc) - pnpm 10 — run
corepack enableto get the pinned version automatically (Node 25+ no longer bundles corepack;npm install -g pnpmworks too)
Setup
- Clone the repository
- Copy the example config file and adjust it to your needs:
cp config.toml.example config.toml - Edit
config.tomlwith your Nextcloud calendar URLs and credentials - Install dependencies:
pnpm install - Start the development server:
make dev
The app will be available at http://localhost:3000. The app looks for
./config.toml by default; set the CONFIG_PATH environment variable (e.g. in
.env.local, see .env.example) to use a different file.
Demo mode
To try the app without a Nextcloud instance, run
make demo
It starts the dev server with config.demo.toml, which
points at the checked-in calendars in demo/ via file:// URLs — no
credentials needed.
Development Features
- Hot reload: Changes to source files are automatically reflected
- TypeScript support with type checking
- ESLint for code quality
- Prettier for code formatting
Useful Commands
make help lists all tasks. The most useful ones:
make dev # start the development server
make check # typecheck + lint + format check + tests (same as CI)
make test # run the test suite
make format # format the codebase with prettier
make image # build the container image locally with podman
Deployment
A container image is published to the Forgejo package registry whenever a release is published:
git.hannover.ccc.de/buergerschule/event-display:latest (also tagged with the release tag and the
commit SHA). It is built by CI from the Dockerfile.
The production deployment — rootless Podman with systemd Quadlets, pulling this image via
podman auto-update — is documented in deploy/podman/README.md.
Configuration
There is a config.toml to configure every important aspect of the app. The path for this file can be set using the CONFIG_PATH environment variable. If this variable is not set, the app will look for a config.toml in the current working directory.
Here is an example configuration:
[frontend]
# The title of the page displayed at the top of the page
title = "Veranstaltungen in der Bürgerschule"
# The url of the logo displayed in the top right corner
icon = "/logo.png"
# The theme color, used for everything colored in the app
themeColor = "#EC6500"
# the time (in seconds) for scrolling to the next page of events when pagination is happening because of a lot of events (optional, default: 10)
scrollInterval = 10
# Display scale factor for the day view, like an OS display-scaling setting: scales the whole
# UI (text, spacing, borders, logo) so one build can be tuned to the physical screen it runs
# on. Does not affect /wochenplan. (optional, default: 1, range: 0.5-3)
scale = 1
[calendar]
# The timezone of the calendar
timeZone = "Europe/Berlin"
# The interval (in seconds) to refresh loaded events from the calendar
refreshInterval = 300
# The calendars to load events from
# name: The name of the room the calendar is for (displayed below the title of the event)
# url: The url of the calendar (found as the "Internal URL" in the nextcloud calendar settings)
# location: The location of the room the calendar is for, displayed on the right of the event list (optional, default: "")
# hideName: If the name of the room should be hidden in the event list (below the title) (optional, default: false)
calendars = [
{ name = "Raum A", url = "https://cloud.example.de/remote.php/dav/calendars/kalendar/raum-a/", location = "2. OG", hideName = false },
{ name = "Raum B", url = "https://cloud.example.de/remote.php/dav/calendars/kalendar/raum-b/", location = "2. OG", hideName = false },
]
# events that contain these keywords in their iCal categories will be ignored and not shown in the event list
ignoreKeywords = ["!intern!", "!privat!"]
# keywords that will be removed from event titles and mark events as "open end" if found in iCal categories
openEndKeywords = ["offen", "openend"]
# time in minutes after which an event is considered to be in the past and will be removed from the event list (0 means events will not be removed)
timeout = 0
# the auth information for the nextcloud instance (app password), the user must have read access to the calendars specified above
# type: The type of authentication, currently only "basic" is supported (optional)
auth = { type = "basic", username = "kalendar", password = "abcdefg" }
API
GET /api/hours
Returns the start time of the first event and the end time of the last event of a given day as JSON. All-day events and private events (see ignoreKeywords) are excluded.
Query parameters:
date(optional): the day to query, inYYYY-MM-DDformat. Defaults to the current day if omitted. The date is interpreted in the timezone configured under[calendar] timeZone.
Example:
curl "http://localhost:3000/api/hours?date=2026-06-10"
Example response:
{
"date": "2026-06-10",
"timeZone": "Europe/Berlin",
"first": "2026-06-10T08:00:00+02:00",
"last": "2026-06-10T18:00:00+02:00",
"eventCount": 5
}
first/last: ISO 8601 timestamps of the earliest start and latest end, in the configured timezone (including its UTC offset).nullif there are no matching events.eventCount: the number of events considered.
A request with an invalid date returns HTTP 400; an error while fetching the calendar returns HTTP 502.
GET /api/events
Schedule feed for the info-beamer signage package (package-buergerschule). Returns the events of yesterday, today and the following days as raw data — private events are excluded, but unlike /, no timeout filter is applied (hiding ended events is the renderer's job) and no display strings for header or date are included.
Query parameters:
days(optional): number of lookahead days after today, integer1..14, default5. The response always contains2 + daysday entries (yesterday + today + lookahead); days without events carry an emptyeventsarray.
Example:
curl "http://localhost:3000/api/events?days=3"
Response shape, field semantics and the consuming sync service are documented in the package: package-buergerschule/docs/event-display-api.md. Invalid days returns HTTP 400; a calendar fetch error returns HTTP 502.
GET /no-zoom
Disables the day-view zoom ([frontend] scale) for the browser that opens it, then
redirects to /. Useful when staff open the display page on a laptop or phone, where the
scale factor chosen for the kiosk screen is far too large.
The opt-out is a session cookie: it applies to that one browser only, never to the kiosk
itself, and disappears when the browser is closed. config.toml is not touched, so no
restart is involved.
While it is active, / shows a red bar reading "Der Zoom ist für dieses Gerät
deaktiviert." with a link back to /no-zoom?clear=1, which removes the cookie and
restores the configured zoom. /wochenplan is unaffected either way — it is never zoomed.
License
This software is licensed under GNU Affero General Public License V3. For more details see LICENSE.
Financing
This software has been built from public money. For more info, why publicly funded software should be public code, visit https://publiccode.eu/, an initiative by Free Software Foundation Europe.