diff --git a/fdhview/api/info.py b/fdhview/api/info.py
new file mode 100644
index 0000000..01c9121
--- /dev/null
+++ b/fdhview/api/info.py
@@ -0,0 +1,16 @@
+from pathlib import Path
+
+from fastapi import APIRouter
+from fastapi.responses import FileResponse
+
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+router = APIRouter(
+ prefix="/info",
+ tags=["Info"]
+)
+
+
+@router.get("", response_class=FileResponse)
+async def get_info():
+ return FileResponse(BASE_DIR / "html" / "info.html")
diff --git a/fdhview/api/screenings.py b/fdhview/api/screenings.py
index 5c61cc5..6534124 100644
--- a/fdhview/api/screenings.py
+++ b/fdhview/api/screenings.py
@@ -27,7 +27,7 @@ async def get_infodesk_screenings(
select(Screening)
.where(Screening.start > now)
.order_by(col(Screening.start))
- .limit(10)
+ .limit(6)
)
screenings = await session.exec(query)
return [ScreeningRead.model_validate(s) for s in screenings.all()]
diff --git a/fdhview/html/info.html b/fdhview/html/info.html
new file mode 100644
index 0000000..cfac768
--- /dev/null
+++ b/fdhview/html/info.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+VANDAAG — Filmhuis Den Haag
+
+
+
+
+
+
+
+
+
diff --git a/fdhview/main.py b/fdhview/main.py
index 64e9d18..c2d9397 100644
--- a/fdhview/main.py
+++ b/fdhview/main.py
@@ -5,6 +5,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fdhview.api.admin import router as admin_router
+from fdhview.api.info import router as info_router
from fdhview.api.screenings import router as screenings_router
from fdhview.core.database import init_db
@@ -24,3 +25,4 @@ app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
app.include_router(admin_router)
app.include_router(screenings_router, prefix="/api")
+app.include_router(info_router)
diff --git a/fdhview/static/info.js b/fdhview/static/info.js
new file mode 100644
index 0000000..1682d54
--- /dev/null
+++ b/fdhview/static/info.js
@@ -0,0 +1,63 @@
+const POLL_MS = 5 * 60 * 1000;
+
+// "2026-10-13T17:30:00" -> "17:30"
+function hhmm(iso) {
+ return iso.slice(11, 16);
+}
+
+// map kijkwijzer codes to /static/.png
+function kijkwijzerIcons(codes) {
+ return (codes || []).map(c =>
+ `
`
+ ).join("");
+}
+
+function renderFilms(films) {
+ document.getElementById("films").innerHTML = films.map(f => {
+ const isBoutique = f.screen === "6";
+ return `
+
+
${hhmm(f.start)}
${hhmm(f.end)}
+
${kijkwijzerIcons(f.kijkwijzer)}
+
+
${f.title}
+
+ ${isBoutique
+ ? `Zaal 6 - Boutique Cinema`
+ : `Zaal ${f.screen}`}
+ ${f.en_subs ? "EN subs" : ""}
+
+
+ ${f.seats_sold / f.seats_total > 0.85 ? '
Laatste kaarten
' : "
"}
+
`;
+ }).join("");
+}
+function renderPrices(prices) {
+ const row = p => `${p.label}${p.price}
`;
+ document.getElementById("prijzen").innerHTML = prices.prijzen.map(row).join("");
+ document.getElementById("kortingsprijzen").innerHTML = prices.kortingsprijzen.map(row).join("");
+}
+
+function renderClock() {
+ const d = new Date();
+ document.getElementById("date").textContent =
+ d.toLocaleTimeString("nl-NL", { hour: "2-digit", minute: "2-digit" });
+}
+
+async function refresh() {
+ try {
+ const [filmsRes, pricesRes] = await Promise.all([
+ fetch("/api/screenings"),
+ fetch("/api/screenings/prices"),
+ ]);
+ renderFilms(await filmsRes.json());
+ renderPrices(await pricesRes.json());
+ } catch (err) {
+ console.error("refresh failed, keeping current display:", err);
+ }
+}
+
+renderClock();
+refresh(); // initial paint
+setInterval(refresh, POLL_MS); // poll every 5 minutes
+setInterval(renderClock, 30_000); // keep the clock accurate
diff --git a/fdhview/static/style.css b/fdhview/static/style.css
index 2088656..81b441e 100644
--- a/fdhview/static/style.css
+++ b/fdhview/static/style.css
@@ -24,15 +24,26 @@ body {
}
.film {
display: grid;
- grid-template-columns: 90px 60px 1fr 170px;
- gap: 12px; align-items: start;
+ grid-template-columns: 90px 160px 1fr 170px;
+ gap: 14px; align-items: start;
padding: 16px 0 18px;
border-top: 1px solid var(--rule);
}
.times { line-height: 1.25; }
.times .start { font-size: 20px; font-weight: 600; }
.times .end { font-size: 20px; color: var(--muted); }
-.icons { display: flex; gap: 6px; padding-top: 3px; }
+.icons {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 6px;
+ align-items: center;
+ padding-top: 3px;
+}
+.icons .kijkwijzer {
+ width: 26px; height: 26px;
+ object-fit: contain;
+ display: block;
+}
.icons .dot {
width: 26px; height: 26px; border-radius: 50%;
background: var(--badge); color: #fff;
@@ -49,7 +60,8 @@ body {
.pill {
background: #b9bfc9; border-radius: 14px;
padding: 4px 14px; font-size: 12px; font-weight: 600;
- letter-spacing: 1px; text-transform: uppercase;
+ letter-spacing: 1px;
+ text-transform: none; /* "Boutique Cinema" reads like the photo */
}
.badge-last {
justify-self: end; align-self: start; margin-top: 3px;