No description
  • JavaScript 72.2%
  • CSS 16.7%
  • HTML 9.9%
  • Dockerfile 1.2%
Find a file
2026-10-26 15:22:28 +01:00
public feat: Add TV screens for bookings, weather, NOTAMs, and traffic 2026-10-24 14:08:00 +02:00
src refactor: simplify logic in parseItems function by removing unnecessary condition for starting new items 2026-10-26 15:22:28 +01:00
.containerignore first commit 2026-10-24 13:12:34 +02:00
.env.example feat: Add TV screens for bookings, weather, NOTAMs, and traffic 2026-10-24 14:08:00 +02:00
.gitignore first commit 2026-10-24 13:12:34 +02:00
akg-backamo.container feat: Add TV screens for bookings, weather, NOTAMs, and traffic 2026-10-24 14:08:00 +02:00
Containerfile feat: Add TV screens for bookings, weather, NOTAMs, and traffic 2026-10-24 14:08:00 +02:00
package-lock.json first commit 2026-10-24 13:12:34 +02:00
package.json first commit 2026-10-24 13:12:34 +02:00
README.md feat: Add TV screens for bookings, weather, NOTAMs, and traffic 2026-10-24 14:08:00 +02:00

AKG Backamo ESGA

Airfield dashboard for Aeroklubben i Göteborg at Backamo (ESGA):

  • Live view – latest webcam JPEG (uploaded by the camera via FTP), with a "UTC Time" label added after the timestamp
  • METAR / TAF – nearby airports, from aviationweather.gov
  • Bookings – today's bookings as a timeline, from the myWebLog API v5
  • NOTAM – filtered from the LFV "ESAA FIR VFR 24hr" bulletin for a local flight from ESGA
  • Traffic – live ADS-B on a MapLibre GL JS map, with club aircraft highlighted
  • Clubhouse TV screens – two full-screen views for 16:9 TVs: live traffic, and bookings + weather + NOTAM + significant weather chart

Access requires a myWebLog login (TV screens use a separate token).

How sign-in works

myWebLog's OAuth 2.0 is client credentials only (server to server); there is no member-facing "log in with myWebLog" page. The app therefore:

  1. exchanges the club's API token for a one-hour access token (POST /oauth/token);
  2. checks the member's own username and password with POST /login-checks;
  3. keeps the member signed in with a signed, HTTP-only cookie (7 days by default).

Members can type their username with or without the <organization id>- prefix. Sign-in attempts are rate-limited (10 per 15 min per IP), and myWebLog applies its own lock-out after too many failed attempts.

Data sources

Panel Source Refresh
Webcam Files in the webcam volume 30 s
METAR aviationweather.gov/api/data/metar + /taf 5 min
Bookings api.myweblog.se/main/v5/bookings + /objects 2 min
NOTAM aro.lfv.se – PIB "ESAA FIR VFR 24hr" (PDF) 10 min
Traffic api.adsb.lol (or a local readsb/tar1090 aircraft.json) 5 s
SWC Northavimet NORCICS chart (PDF, © FMI/SMHI/DMI/MET Norway), converted to PNG 10 min
Map OpenFreeMap tiles (no key needed) –

Upstream responses are cached server-side and shared between users. If a source fails, the last good data is served and marked stale.

NOTAM disclaimer: the filter keeps NOTAMs for the configured aerodromes, en-route items with a position within NOTAM_RADIUS_NM of ESGA, and items mentioning nearby places. It is a convenience, not an official briefing – the panel links to the complete bulletin.

Deployment (Podman Quadlet)

1. myWebLog API token

On the myWebLog website (Integrations → API), create a token with read access to:

  • Login check
  • Bookings
  • Objects

Note the token (mwla1_…) and its Client ID. If the token is restricted to IP addresses, add the server's public IP.

2. Build the image

podman build -t akg/backamo:latest /path/to/akg-backamo

3. Create the secrets

printf '%s' 'mwla1_your_token' | podman secret create akg-backamo-mwl-secret -
openssl rand -base64 48 | tr -d '\n' | podman secret create akg-backamo-session-secret -
openssl rand -hex 24 | tr -d '\n' | podman secret create akg-backamo-tv-token -

Changing the session secret signs everyone out. Read the TV token back with podman secret inspect --showsecret akg-backamo-tv-token when setting up the screens.

4. Webcam volume

The container reads the camera images from a Podman volume shared with the FTP server, mounted read-only at /webcam. The default volume name is backamo-webcam; change it in the unit file if yours differs.

  • By default the newest *.jpg/*.jpeg is shown (files modified in the last 2 s are skipped while the upload completes). Set WEBCAM_FILE if the camera always overwrites the same file name.
  • The files must be readable by the container user (uid 1000, node).
  • The unit mounts with z (shared SELinux label), not Z, so the FTP container keeps access.

5. Install the Quadlet unit

Edit akg-backamo.container – at least MWL_CLIENT_ID and the volume name – then:

# System-wide
sudo cp akg-backamo.container /etc/containers/systemd/
sudo systemctl daemon-reload
sudo systemctl start akg-backamo

# or rootless
cp akg-backamo.container ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start akg-backamo

Quadlet-generated services are started at boot through the [Install] section; systemctl enable is not needed.

Logs and status:

journalctl -u akg-backamo -f        # add --user for rootless
podman healthcheck run akg-backamo

6. Reverse proxy

The app is published on the host at port 9197 and expects a TLS-terminating reverse proxy in front (session cookies are Secure).

Caddy in a container (pasta)

In a pasta-networked container, host.containers.internal is translated to the host's own address, not to 127.0.0.1. The port must therefore be published on all interfaces (PublishPort=9197:3000, as in the unit file), and blocked from outside by the host firewall:

backamo.example.se {
    encode zstd gzip
    reverse_proxy http://host.containers.internal:9197
}

No extra headers are needed. Caddy forwards the original Host, and sets X-Forwarded-Proto: https and X-Forwarded-For to the real client address, overwriting any value the client sent. The app needs the original Host and the https scheme for its same-origin check on sign-in, and X-Forwarded-For for the sign-in rate limit.

Reload Caddy after editing, e.g. podman exec caddy caddy reload --config /etc/caddy/Caddyfile.

To check the connection from inside the Caddy container:

podman exec caddy wget -qO- http://host.containers.internal:9197/healthz

Caddy on the host

The port can then be published on loopback only (PublishPort=127.0.0.1:9197:3000):

backamo.example.se {
    reverse_proxy 127.0.0.1:9197
}

nginx

nginx needs proxy_set_header Host $host;, proxy_set_header X-Forwarded-Proto $scheme; and proxy_set_header X-Forwarded-For $remote_addr;.

Trusted proxy

TRUST_PROXY=1 trusts exactly one proxy hop, i.e. the X-Forwarded-* headers set by Caddy. Make sure port 9197 is not reachable from outside the host (firewalld and most default firewall setups block unlisted ports). If it is reachable directly, clients bypass TLS and can send their own X-Forwarded-For to get around the sign-in rate limit.

With firewalld, check that the port is not open:

sudo firewall-cmd --list-ports    # 9197/tcp must not be listed

Updating

podman build -t akg/backamo:latest /path/to/akg-backamo
sudo systemctl restart akg-backamo

Clubhouse TV screens

Two full-screen pages designed for 1920×1080 (they scale to any 16:9 screen, and never need scrolling or a mouse):

URL Content
/tv/traffic Live traffic map around ESGA (larger symbols, 5/10/20/30 NM rings) and a list of aircraft sorted by distance, club aircraft first
/tv/board Today's bookings, METAR/TAF, NOTAM, and the significant weather chart

Long lists (METAR/TAF, NOTAM, aircraft) scroll slowly by themselves. The pages reload every 6 hours to pick up new versions.

Signing in a screen

The TVs don't use a member login. Open each page once with the TV token:

https://backamo.example.se/tv/traffic?tv_token=<TV_TOKEN>
https://backamo.example.se/tv/board?tv_token=<TV_TOKEN>

The app sets a cookie valid for 400 days and redirects to the address without the token. The cookie is renewed daily while the screen is on, so it doesn't expire. Bookmark or set the start page to the address without the token.

To sign out all screens, change TV_TOKEN and the session secret. Wrong tokens get 403 and are logged.

Significant weather chart

The board shows the NORCICS SWC charts (SFC–FL450) issued for 00, 06, 12 and 18 UTC. It shows the chart for the latest slot before now and the next one, e.g. at 15Z the 12Z and the 18Z charts, alternating every SWC_SLIDE_SECONDS. A chart not yet published is skipped. The PDFs are converted to PNG in the container with pdftoppm.

Browser on the TV

Any kiosk browser works, e.g. a Raspberry Pi with Chromium:

chromium --kiosk --noerrdialogs --disable-session-crashed-bubble https://backamo.example.se/tv/board

The map needs WebGL, which most smart-TV browsers lack; a small computer (Raspberry Pi 4/5, mini PC) on the HDMI input is the reliable option.

Configuration

All settings are environment variables (Environment= in the unit, or .env for development).

Variable Default Description
SESSION_SECRET required Cookie signing key
MWL_CLIENT_ID required myWebLog token Client ID
MWL_CLIENT_SECRET required myWebLog token (mwla1_…)
PORT 3000 Listen port inside the container
TRUST_PROXY loopback Express trust proxy setting
COOKIE_SECURE true Set false only for local http development
SESSION_MAX_AGE_HOURS 168 Sign-in lifetime
TZ_LOCAL Europe/Stockholm Time zone for bookings
BOOKING_AIRCRAFT (all) Registrations shown in the booking calendar, comma-separated, in display order (e.g. SE-MOA,SE-MOY,SE-KWH)
CLUB_AIRCRAFT BOOKING_AIRCRAFT Registrations highlighted on the traffic map. With adsb.lol they are also looked up by registration (every 15 s), so they show even outside the radius, and are listed under the map when heard without a position (Mode S only)
AIRFIELD_LAT / _LON 58.1772 / 11.9735 Map centre, NOTAM and ADS-B reference
WEBCAM_DIR /webcam Folder with camera images
WEBCAM_FILE (newest jpg) Fixed file name
WEBCAM_LABEL UTC Time Text added after the timestamp; empty disables
WEBCAM_LABEL_X 800 x where the timestamp ends (px)
WEBCAM_LABEL_BASELINE 1702 Bottom of the timestamp text (px)
WEBCAM_LABEL_HEIGHT 50 Timestamp text height (px)
WEBCAM_REFRESH_SECONDS 30 Browser polling interval
METAR_STATIONS ESGT,ESGP,ESGG,ESIB METAR/TAF stations
NOTAM_AERODROMES ESGA,ESGT,ESGP,ESGG,ESIB Aerodromes whose NOTAMs are always shown
NOTAM_RADIUS_NM 40 Radius for positioned en-route NOTAMs
NOTAM_KEYWORDS nearby place names Matches NOTAMs without a position
NOTAM_BULLETIN ESAA FIR VFR 24hr LFV bulletin name
ADSB_URL https://api.adsb.lol/v2/point/{lat}/{lon}/{radius} ADS-B source; {lat}, {lon}, {radius} substituted
ADSB_RADIUS_NM 40 ADS-B search radius
ADSB_REFRESH_SECONDS 5 Traffic polling interval
MAP_STYLE_URL https://tiles.openfreemap.org/styles/liberty MapLibre style
MAP_EXTRA_ORIGINS – Extra tile/glyph origins allowed by the CSP (comma-separated)
TV_TOKEN (TV screens disabled) Token for signing in the clubhouse screens (at least 24 characters)
SWC_URL Northavimet NORCICS URL with {time} Significant weather chart PDF; {time} is YYYYMMDDHHmm
SWC_SLIDE_SECONDS 15 Time each chart is shown on the board

Local ADS-B receiver

If the club runs its own receiver (readsb, dump1090-fa, tar1090), point ADSB_URL at its aircraft.json, e.g. http://192.168.1.50:8080/data/aircraft.json. A local feed has no registration/type data, only what the aircraft transmits.

Other map styles

A style using another tile host must have its origin in MAP_EXTRA_ORIGINS (the CSP blocks it otherwise). Labels use the Noto Sans Regular font, which the style's glyph server must provide.

Local development

Requires Node.js 22+.

npm install
cp .env.example .env      # fill in the myWebLog credentials
mkdir -p dev-webcam       # put a test .jpg here
npm run dev               # http://localhost:3000

Project layout

Containerfile            Image definition (node:22-bookworm-slim + DejaVu fonts + poppler-utils)
akg-backamo.container    Podman Quadlet unit
src/
  server.js              Express app, routes, CSP, sessions
  config.js              Environment variables
  auth.js                Sign-in / sign-out, TV token, auth middleware
  myweblog.js            myWebLog API v5 client (OAuth, login check, bookings)
  webcam.js              Latest image + "UTC Time" label (sharp)
  weather.js             METAR/TAF
  notam.js               LFV PIB download, parsing and filtering
  adsb.js                ADS-B traffic
  swc.js                 Significant weather charts (PDF → PNG)
  cache.js               Shared TTL cache
public/
  index.html, app.js     Dashboard
  js/                    Shared frontend modules (panels, traffic map, helpers)
  tv/                    Clubhouse TV screens
  login.html             Sign-in page
  assets/                Logo, styles, login script (public)

Colours

Defined as CSS variables in public/assets/styles.css, converted from the club's CMYK values (approximate):

Name CMYK Screen
AKG blue C100 M81 Y20 K4 #12488c
AKG gold C0 M23 Y99 K0 #fdc300
AKG pale gold C7 M13 Y69 K0 #efd56b
AKG red C0 M96 Y66 K0 #e31b44