VanocniProgram
Archiv vánočního TV programu ČT1, ČT2, Novy a Primy na 24.–26. prosince napříč roky.
Christmas TV archive (Vánoční program)
A single-page app for browsing the Czech Christmas TV schedule of ČT1, ČT2, Nova and Prima
on 24, 25 and 26 December across the years. The release ships the finished data set in
data/program.json; show metadata and links come from ČSFD (cached in data/csfd-cache.json).
Live: https://vanocni-program.tomlabs.xyz · health: https://vanocni-program.tomlabs.xyz/health
Layout
| Path | What it is |
|---|---|
server.js |
Dependency-free Node server: static files, /health, the Logs page |
src/logger.js |
Structured logger: daily rolling JSON files, in-memory ring buffer |
public/ |
The SPA: index.html, app.js (program view), cluster.js (timeline merge), stats.js + stats-view.js (statistics), styles.css, icons |
test/ |
Unit tests for the timeline merge and the statistics (npm test) |
docs/ |
ARCHITECTURE.md and the decision records in docs/decisions/ |
admin/logs.html |
Logs page, served only to the administrator |
data/program.json |
Release data set served to the browser |
data/stats-exclude.json |
Recurring non-Christmas titles the Statistics view leaves out (see ADR 0004) |
scripts/ |
Rebuilding the data set from the local archive and ČSFD |
deploy/ |
VPS deploy script and nginx site config |
Running locally
npm run dev
The site runs on http://127.0.0.1:8132/. The version shown in the footer comes from
package.json and is injected by the server.
Rebuilding the data
npm run prepare:release
Builds the archive from the local sources (data/sources, not committed), applies the stored
ČSFD cache, validates the data set and checks the JavaScript. New ČSFD metadata is fetched
separately with npm run enrich:csfd.
ČSFD posters
npm run enrich:csfd looks up every title that is a film or may be one (type film, děti or untyped) that is not
in data/csfd-cache.json yet, through the ČSFD API wrapper at csfd-api.tomlabs.xyz. It is polite by design:
about one request per second (--delay), a descriptive User-Agent, every result cached (also misses, so a title is never
asked twice), the cache saved every 10 titles and on Ctrl+C, so the run can be stopped and resumed. Matches that are not a
film or TV film are recorded as not_film and skipped. npm run apply:csfd writes the cache into program.json.
npm run validate:data prints the picture coverage. Posters are hotlinked from ČSFD, never copied into the repository.
Adding the current year
Requires Node 22+, Windows PowerShell, and Python 3 with lxml (python -m pip install lxml).
npm test and npm run check also run the offline Python fixture tests (on Linux: python3 with lxml).
Start from a clean working tree with the committed archive and ČSFD cache. Each December run:
npm run update:year # defaults to the current year
npm test
npm run check
git diff --stat
git diff -- data/program.json data/csfd-cache.json
The command downloads only 24–26 December of that year from Česká televize (ČT1, ČT2) and Program.sk (Nova, Prima), at the existing 150 ms pause between source requests. All six pages must identify the requested date and contain full-day coverage for all four channels: at least eight shows per channel/day, a morning start by 09:00, a late-evening start at or after 21:00, a prime-time start between 18:00 and 22:00, and no gap over six hours. Invalid times or duplicate show IDs also stop the update. These conservative guards catch empty and truncated pages; they cannot certify the broadcaster's editorial completeness.
If the schedule is not published yet, or a page is partial, the command exits with code 1 and reports each affected channel/day. The archive, source cache and ČSFD cache stay unchanged. Retry when the broadcasters publish the missing days (usually a few weeks before Christmas). Other HTTP/network errors also fail without publishing staged files.
After the checks, only previously uncached film-like titles are looked up through the existing ČSFD matcher,
with its one-second delay and retry policy. Existing cache entries, including misses and errors, are preserved.
The cache is applied only to the selected year. The merged archive is validated before sources, new cache entries
and data are replaced locally. An interrupted lookup or validation failure leaves the published files unchanged;
lookups staged in that failed attempt will need repeating. An interrupted process may leave data/.update-year.lock;
remove it only after confirming no update is running.
Re-run the command to pick up schedule corrections until broadcast. The UI labels that year's programme preliminary until 27 December. An unchanged parsed/enriched programme retains its original provenance and timestamp, even when advertising markup changes the HTML size. Other years are preserved, including the legacy Christmas Eve snapshot. Explicit years from 2023 through the current year are supported:
npm run update:year -- -Year 2025
Review the diff, update the release version and changelog as appropriate, and commit locally. Have the
Software Architect review and merge; push and deployment still require Tomáš's explicit approval.
Do not run prepare:release as part of this routine: it rebuilds all historical years and needs their local sources.
The full source fetch keeps its previous-year default; an explicit range can include the current year:
npm run fetch:sources -- -StartYear 2026 -EndYear 2026
See ADR 0005 for the staging and replacement decision.
Configuration
| Variable | Default | Meaning |
|---|---|---|
HOST / PORT |
127.0.0.1 / 8132 |
Listening address |
LOG_DIR |
~/.vanocni-program/logs |
Daily log files (app-YYYY-MM-DD.log), outside the app folder |
LOG_LEVEL |
info |
verbose, debug, info, warn, error, fatal |
LOG_LEVEL_OVERRIDES |
– | Per-source levels, e.g. http=debug,health=verbose |
LOG_RETENTION_DAYS |
14 |
How many daily files are kept |
ADMIN_PASSWORD |
– | Enables the Logs page at /logs (HTTP basic auth, any user name). Without it the page answers 404. |
Health
GET /health answers { "status": "Healthy", "version": "0.9.0", "name": "vanocni-program", "entries": { … } }.
archive is Unhealthy when data/program.json cannot be loaded; log is Degraded when log files
cannot be written.
Deployment
azure-pipelines.yml checks the code and data, builds the Docker image, smoke-tests it and on master
ships the image to the VPS, where deploy/deploy-vps.sh swaps the container, waits for /health
(rolling back when the new build is not healthy) and installs the nginx site. After a successful deploy
the pipeline updates the catalogue entry on https://apps.tomlabs.cz.
- Variable group
vanocni-program-deployment:VPS_SSH_PRIVATE_KEY_B64orVPS_SSH_PRIVATE_KEY. - Variable group
appstore-publish:APPSTORE_TOKEN. - On the VPS,
/opt/vanocni-program/.envholdsADMIN_PASSWORD; logs are in/opt/vanocni-program/logs.
Release: bump version in package.json and add its section to CHANGELOG.md in the same commit.
Changelog
Changelog
0.9.0 — 2026-10-18
- Added: one-command current-year import (
npm run update:year) with full-day checks and clear per-channel/day reports when the Christmas programme is not published yet. - Added: safe repeat imports replace only the selected year, enrich only new titles using the existing ČSFD cache and validate before writing.
- Added: a preliminary programme notice until 27 December of the selected year.
- Fixed: explicit source-fetch ranges now include the requested current year and respect the starting year on both sites; the full generator accepts current-year sources.
0.8.1 — 2026-10-18
- Fixed: the Statistics page no longer counts recurring non-Christmas shows (for example Etiket, Vodnické zpívánky, Pod pokličkou, Comeback, Ulice, Do-re-mi, Hřiště) among the most-aired films and the classics; the next titles move up. The program view is unchanged.
- Changed: the shows left out of the statistics come from a versioned list (
data/stats-exclude.json).
0.8.0 — 2026-10-17
- Added: a completely new look with poster cards for films, a sticky header, day tabs, a scrollable year strip, the ČSFD rating on every matched film, and a detail with a large poster and the other years the film aired.
- Added: a Statistics page: the films aired most often (with the typical start and channel), a title-by-year grid of recurring classics, an hour-by-channel/type heatmap, and the prime-time pick of every year.
- Added: a System / Light / Dark theme switch in the header (System is the default and follows the device).
- Added: posters and ČSFD details for many more films.
- Changed: shows that start within 15 minutes of each other now share one row of the timeline instead of staggered rows; short fillers such as the weather are shown as one slim line.
- Changed: the program works properly on a phone: a collapsing menu, stacked cards instead of a sideways-scrolling table, larger touch targets.
- Changed: views have shareable addresses (
#program/24.12/2010,#stats). - Changed: pictures now come only from the archive data (ČSFD); the page no longer asks Wikipedia for images and no longer loads a stock photo.
0.7.1 — 2026-10-01
- Fixed: on larger screens the year list was cut off at the bottom and the most recent years (including the selected one) could not be reached; the list now scrolls on its own and opens at the selected year.
- Fixed: on phones the year strip can also be scrolled with a mouse wheel.
0.7.0 — 2026-09-24
- Added:
GET /healthwith the running version, used by the deploy and the app store. - Added: application log in daily files and a Logs page for the administrator.
- Added: application icon and favicon.
- Changed: the archive data is sent compressed (about 0.5 MB instead of 12 MB) and is only re-downloaded when it changes.
- Changed: the version in the footer now always matches the deployed build.
- Changed: the server only serves the site's own files, not the whole application folder.
- Fixed: a malformed address no longer brings the server down.
0.6.0 — 2026-08-14
- First public release: program of ČT1, ČT2, Nova and Prima for 24–26 December across the years, search, filters by day, year, station and type, show details with ČSFD metadata.