//nbkelley /homelab

Homelab Dashboard

Homelab Dashboard#

What Was Established#

A Node.js + Express dashboard is deployed at https://status.nbkelley.com, serving homelab monitoring data. All API calls are server-side — no internal IPs, credentials, or raw API responses reach the browser.

Deployment#

Detail Value
Public URL https://status.nbkelley.com
Host proxy VM (192.168.1.222)
Port 3002
Runtime Node.js + Express, Docker container
compose.yaml location /home/iluvatar/compose.yaml on proxy VM
App directory /opt/homelab-dashboard/ on proxy VM
Routing Cloudflare Tunnel → 127.0.0.1:3002

File Structure#

/opt/homelab-dashboard/
  server.js         ← Express app, all API logic
  package.json
  package-lock.json
  dockerfile
  start.sh
  node_modules/
  public/
    index.html
    styles.css
    app.js          ← frontend render engine

compose.yaml Entry#

homelab-dashboard:
  build: /opt/homelab-dashboard
  container_name: homelab-dashboard
  restart: unless-stopped
  network_mode: host
  environment:
    - PORT=3002

network_mode: host means the app binds directly to host port 3002. The ports: mapping is ignored when using host networking.

MBTA Dashboard - Frontend Implementation

MBTA Dashboard - Frontend Implementation#

What Was Established#

A portrait-mode (1080x1920) transit dashboard displaying real-time MBTA departures, commuter rail, weather, news, and a static transit map. Hosted on PLT-MBTADisplay (192.168.168.42), served via Nginx with a Node/Express proxy that handles all external API calls server-side. Target display is a 1080p vertical screen driven by Anthias on a Raspberry Pi 3B+.

Design Evolution#

The dashboard went through several major design iterations:

  1. Initial: Line-based cards with MBTA colors, horizontal layout for 1080p TV
  2. Minimalist redesign: White background, grey dividers, MBTA colors only on small abbreviated line pills (RL, OL, BL, GL-B/C/D/E), smaller station-only headers with walk time
  3. Station-based cards: Cards grouped by station (South Station, State Street, Park Street) rather than by line, with all routes listed per station
  4. Final portrait layout: 2-column CSS Grid, flex column containers, flat time-sorted departure list with line pills, full-width static map image at bottom

Layout (1080x1920 Portrait)#

  • 2-column, 3-row card grid above a full-width static map image
  • Cards flex with content height (no preset card height)
  • CSS Grid with grid-template-columns: 1fr 1fr

Key Features#

CSS Ticker/Marquee#

  • Header text that overflows its container scrolls seamlessly in a ticker
  • Uses CSS animation with transform: translateX() — no JavaScript
  • Animation speed set slow for readability

Station-Based Grouping#

  • Cards grouped by physical station: South Station, State Street, Park Street
  • Each station card shows all lines serving that station
  • Departures sorted by time, each with abbreviated line pill (RL, OL, BL, GL-B, etc.)
  • Filtered to show departures within 10 minutes walking distance

Server-Side Proxy Architecture#

  • All API calls proxied through Node/Express on localhost:3000
  • Client fetches single /api/data endpoint — no direct browser-to-MBTA calls
  • API keys stored in environment variables, never exposed to browser
  • Server-side caching with TTL to reduce upstream API calls

News Ticker#

  • Pulls from multiple RSS feeds (MassLive Boston, State House News)
  • Displays first 3 items from each feed as <title> - <description>
  • Modular RSS feed configuration for easy addition of new sources

Static Map#

  • Originally Leaflet.js with OpenStreetMap and hardcoded markers
  • Replaced with static image for performance on Raspberry Pi
  • Generated via Snazzy Maps + PowerPoint, served as local webp file
  • Full-width, placed below the card grid

Performance & Optimization#

Memory Constraints (Raspberry Pi 3B+)#

  • Total system memory: 788MB
  • Dockerized Anthias viewer consumes significant resources
  • Strategies employed:
    • Server-side proxy: Eliminates duplicate API calls from browser
    • Lazy loading: Defer non-critical rendering
    • DOM optimization: Remove unused elements, minimize reflows
    • Image optimization: Convert to WebP, use static image over dynamic map
    • Cache management: Server-side cache with periodic clearing
    • Swap avoidance: Swap degrades SD card — keep memory footprint below physical RAM

Font Standardization#

  • Default to Arial for cross-platform consistency
  • Helvetica Neue as preferred fallback
  • Qt WebEngine (Anthias) renders fonts differently than desktop Chrome

Current Configuration#

  • Host: PLT-MBTADisplay (192.168.168.42)
  • Web root: /var/www/MBTADisplay/public
  • Proxy: /opt/mbta-proxy/server.js (Node/Express, port 3000)
  • Process manager: pm2 with systemd service (pm2-administrator)
  • Deployment: Git push → GitHub → manual pull on server
  • Access URLs: http://transit.intra.plgt.com (internal), https://mbtadash.nbkelley.com (Cloudflare)

Historical Notes#

  • Conversation dates: 2026-03-12 to 2026-04-12.
  • Collaborative workflow: Claude AI (creative/prompt engineering) → Claude Code (implementation).
  • Blue Bikes section was added, then fully removed when GBFS feed deprecated.
  • Ferry panel configuration evolved significantly — seasonal routes removed seasonally.
  • Several Claude Code UI hallucinations corrected by user during Bambu Studio discussion.

Sources#

  • ingested/chats/159-Create MBTA Train Dashboard with API.md
  • Claude Code conversation: “MBTADashboard - Prompt Maker” (chat 24)

MBTA Dashboard - Kiosk Mode

MBTA Dashboard - Kiosk Mode#

What Was Established#

The MBTA dashboard runs as a 24/7 kiosk display on a Raspberry Pi 3B+ using Anthias (formerly Screenly OSE) in Docker. The display is portrait 1080x1920. The Pi has severe memory constraints (788MB total) requiring aggressive optimization.

Anthias Deployment#

Hardware#

  • Device: Raspberry Pi 3B+ (788MB RAM)
  • Display: 1080x1920 portrait screen
  • Software: Anthias (Docker-based digital signage)

Display Configuration#

  • Dashboard page set as primary asset
  • Splash page appears for 1 minute every 11 hours 59 minutes as a refresh cycle
  • Page refreshes via cron job to prevent memory leaks

Cron Refresh#

# Refresh the kiosk page periodically to clear memory
0 */6 * * * docker restart screenly-anthias-viewer-1

Qt WebEngine Quirks#

Anthias uses Qt WebEngine for rendering, which differs from desktop Chrome:

MBTA Dashboard - Setup

MBTA Dashboard - Setup#

What Was Established#

Office transit dashboard deployed on a self-hosted Debian VM (PLT-MBTADisplay, 192.168.168.42). Nginx serves static files from /var/www/MBTADisplay/public and proxies /api/ requests to a Node/Express caching proxy on port 3000. API keys are stored server-side and never exposed to the browser. Process managed via pm2 with a systemd service.

Architecture#

Browser (Anthias/Desktop)
    → Nginx (:80) → / → static files (/var/www/MBTADisplay/public)
                   → /api/ → Node/Express proxy (:3000)
                                → MBTA v3 API
                                → OpenWeatherMap API
                                → RSS feeds
                                → Caches responses

Nginx Configuration#

server {
    listen 80;
    server_name transit.intra.plgt.com 192.168.168.42;

    root /var/www/MBTADisplay/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Node/Express Proxy#

Setup#

mkdir -p /opt/mbta-proxy
cd /opt/mbta-proxy
npm init -y
npm install express node-fetch

API Key Management#

  • API keys stored in /opt/mbta-proxy/.env
  • Loaded via process.env.MBTA_API_KEY in server.js
  • pm2 started with --env flag to load .env file
  • Critical: API key must survive server.js overwrites from GitHub syncs

pm2 Process Manager#

pm2 start server.js --name mbta-proxy
pm2 save
pm2 startup systemd

systemd Service (/etc/systemd/system/pm2-administrator.service)#

[Unit]
Description=PM2 process manager
After=network.target

[Service]
Type=forking
User=administrator
ExecStart=/usr/local/bin/pm2 resurrect
ExecReload=/usr/local/bin/pm2 reload all
ExecStop=/usr/local/bin/pm2 kill
Restart=on-failure

[Install]
WantedBy=multi-user.target

GitHub Deployment#

Repository#

  • Repo: https://github.com/bich-nguyen/MBTADisplay.git
  • Cloned to /var/www/MBTADisplay
  • Static files in public/ subdirectory
  • Server files in /opt/mbta-proxy/ (separate from web root)

Ownership#

sudo chown -R administrator:administrator /var/www/MBTADisplay

Note: www-data ownership breaks git operations from administrator user.