README.md · last modified 2026-04-14 08:22
Dedicated web dashboard for managing and analysing MoSART soundscape measurements at BrabantZorg care locations. Built with FastAPI, HTMX and Plotly — no JavaScript frameworks required.
cd brabantzorg_app
pip install -r requirements.txt
python start_server.py
The dashboard opens at http://127.0.0.1:8002.
The server has access to MoSART measurement data worldwide, but this application enforces strict location-based restrictions. Every data fetch — whether from the dashboard or the API proxy — is validated against an allowlist of approved locations.
How it works:
- The [security] section in config/brabantzorg.toml defines which locations may be queried and the maximum radius around each.
- Any request for coordinates outside an approved zone is rejected.
- The fetch radius is silently capped to the zone's max_km (e.g. a request for 50 km around Sint Jan is capped to 1.0 km).
- This is enforced server-side in modules/location_guard.py — it cannot be bypassed by the client.
Currently approved:
| Location | Coordinates | Max radius |
|---|---|---|
| Sint Jan (Uden) | 51.6604, 5.6108 | 1.0 km |
Adding a new location — edit config/brabantzorg.toml and add a [[security.allowed_locations]] block:
[[security.allowed_locations]]
name = "Heelwijk"
latitude = 51.7341812
longitude = 5.5329213
max_km = 1.0
Heelwijk and Hofstaete are already templated (commented out) in the config for easy activation.
Two layers of access control:
1. Location guard (brabantzorg.toml → [security]) — master allowlist. No data outside these zones ever leaves the server.
2. API keys (api_keys.toml) — per-client restrictions. A Chromebook key can be limited to only Sint Jan, while an internal key sees all approved locations.
Three visualisations are rendered side-by-side for Fase 1 and Fase 2:
- Kwadrantverdeling — summary table with counts and percentages per quadrant.
- Beoordelingsruimte — 2D scatter plot (pleasantness × eventfulness) with quadrant colours.
- Kwadrant-cirkeldiagram — pie chart of quadrant distribution, plus optional grouped pie charts by room group.
enable_room_groups / room_groups in config.BZ MoSART {name} - {room}.brabantzorg_app/
├── app.py # FastAPI application & routes
├── start_server.py # Startup script (port 8002)
├── requirements.txt # Python dependencies
├── README.md
├── config/
│ ├── brabantzorg.toml # Project configuration (single source of truth)
│ └── api_keys.toml # API keys for external clients
├── deploy/
│ └── com.soundappraisal.brabantzorg.plist # macOS launchd service
├── modules/
│ ├── mosart_api.py # Self-contained MoSART API client
│ ├── config_loader.py # Read/write TOML config
│ ├── i18n.py # Dutch/English localisation (t() function)
│ ├── implementation_manager.py # Implementation CRUD, grid, fetch
│ ├── data_cache.py # Parquet-based measurement cache
│ ├── viz_handlers.py # HTMX endpoint handlers for visualisations
│ ├── viz_helpers.py # Chart & table generation (matplotlib/plotly)
│ ├── comparison.py # Multi-implementation comparison
│ ├── qr_generator.py # QR code generation & download
│ ├── api_proxy.py # API proxy endpoints for external clients
│ ├── access_control.py # API key validation
│ └── location_guard.py # Location allowlist enforcement
├── static/
│ ├── logo.png # BrabantZorg logo
│ ├── manifest.json # PWA manifest
│ └── service-worker.js # PWA service worker (static asset caching)
├── data/ # Cached parquet files (auto-created)
├── qr_codes/ # Generated QR code images (auto-created)
└── logs/ # Server logs (auto-created)
All settings live in config/brabantzorg.toml:
project_name / description — shown in the page header.default_location — fixed location for new implementations (default: "Sint Jan").language — UI language: "nl" (Dutch, default) or "en" (English).enable_room_groups / room_groups — optional grouping of rooms in visualisations.settings.distance_choice — radius in km for filtering measurements around a location.[[implementations]] — each entry defines a measurement campaign with phase dates, rooms, and visibility.[[locations]] — pre-configured locations with coordinates and sublocations.Change language = "nl" to language = "en" in the TOML file and reload the page. All UI labels, buttons, table headers and status messages will switch to English.
modules/mosart_api.py; no external dependencies on parent directories.The app is deployed on a Mac Mini and exposed to the internet via Tailscale Funnel.
┌─────────────────────┐ HTTPS ┌────────────────────────┐
│ Browser / PWA │ ──────────────▶ │ Tailscale Funnel │
│ (Chromebook, etc.) │ │ (relay + TLS) │
└─────────────────────┘ └───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Mac Mini │
│ /opt/services/ │
│ brabantzorg_app/ │
│ FastAPI :8002 │
└───────────┬────────────┘
│
▼ server-side
┌────────────────────────┐
│ MoSART API (Azure) │
│ sor-mob-prd-backend │
└────────────────────────┘
Key points:
- The browser never contacts the MoSART API directly — all data fetching happens server-side on the Mini.
- API credentials are stored as an environment variable (MOSART_CLIENT_SECRET) on the Mini, never sent to the client.
- Tailscale Funnel provides public HTTPS with automatic Let's Encrypt certificates.
https://soundappraisal-dev-server.tail184f99.ts.net/mosart_bz
From your development machine, sync changes to the Mini:
rsync -av --exclude='__pycache__' --exclude='venv/' --exclude='logs/' --exclude='data/' \
brabantzorg_app/ soundappraisal-dev-server:/opt/services/brabantzorg_app/
The app has auto-reload enabled — changes take effect within seconds.
The app runs as a macOS LaunchAgent (com.soundappraisal.brabantzorg) that auto-starts on boot.
# View status
launchctl list | grep soundappraisal
# Stop
launchctl unload ~/Library/LaunchAgents/com.soundappraisal.brabantzorg.plist
# Start
launchctl load ~/Library/LaunchAgents/com.soundappraisal.brabantzorg.plist
# View logs
tail -f /opt/services/brabantzorg_app/logs/stderr.log
# Enable (runs in background, persists across reboots)
/opt/homebrew/bin/tailscale funnel --bg --set-path /mosart_bz http://localhost:8002
# Disable
/opt/homebrew/bin/tailscale funnel --set-path /mosart_bz off
# Check status
/opt/homebrew/bin/tailscale serve status
| Variable | Description | Where set |
|---|---|---|
MOSART_CLIENT_SECRET |
MoSART API client secret | launchd plist (on the Mini) |
The app exposes proxy endpoints for external/standalone clients that need programmatic access to MoSART data. The dashboard itself does not use these — it calls MoSARTAnalyzer directly server-side.
GET /api/health — health check, no authentication required.
curl https://soundappraisal-dev-server.tail184f99.ts.net/mosart_bz/api/health
# {"status": "ok"}
POST /api/fetch — fetch MoSART measurement data. Requires API key.
curl -X POST https://soundappraisal-dev-server.tail184f99.ts.net/mosart_bz/api/fetch \
-H "Content-Type: application/json" \
-H "X-API-Key: bz-chromebook-2026" \
-d '{"latitude": 51.6604, "longitude": 5.6108, "start_date": "2025-10-20", "end_date": "2025-11-16", "distance_km": 2.5}'
Response: {"records": 123, "data": [...]}
API keys are defined in config/api_keys.toml. Each key can restrict which locations a client may query.
[keys.brabantzorg_chromebook]
secret = "bz-chromebook-2026"
allowed_locations = ["Sint Jan"]
[keys.brabantzorg_internal]
secret = "bz-internal-2026"
allowed_locations = ["*"] # unrestricted
Pass the key via X-API-Key header or ?key= query parameter.
The app can be installed as a Progressive Web App on ChromeOS (or any device with Chrome).
https://soundappraisal-dev-server.tail184f99.ts.net/mosart_bz.static/manifest.json defines the app name, icons, colours and standalone display mode.service-worker.js (served from /service-worker.js) caches static assets (logo, fonts, manifest) for faster loads.Key Python packages: FastAPI, uvicorn, pandas, numpy, plotly, matplotlib, qrcode, tomli-w, requests.
Requires Python 3.11+ (for tomllib).
The dashboard uses BrabantZorg's visual identity:
- Colours: BZ Pink (#c7007d) as primary, BZ Dark Blue (#00203b) as accent.
- Font: Inter (loaded from Google Fonts).
- Logo: static/logo.png (from brabantzorg.eu).
Author: Tjeerd Andringa
© SoundAppraisal BV