A live amateur-radio contest scoreboard. HamStats listens to UDP broadcasts from the N1MM+ contest logger, stores them in a SQLite database, and serves a real-time web dashboard showing radios, contacts, and the running score breakdown — built for multi-operator events like ARRL Field Day.
It's designed to run as a passive wall/kiosk display — left up on a screen for everyone to watch, with no mouse or keyboard needed during operation. Admin functions (a live N1MM packet log and settings, including the local time zone used for all displayed times) are tucked behind the app-bar menu so the dashboard stays clean for viewing.
N1MM+ logger ──UDP:12060──▶ HamStats.Website ──▶ SQLite
(XML broadcasts) (N1MMWatcher) │
│ │
/api/v0/* ◀───────────────┘
│
HamStats.Vue (dashboard, polls every 1s)
N1MM+ is configured to broadcast contact, radio, and score messages over UDP. The N1MMWatcher
background service receives them on port 12060, parses the XML, and persists it. The dashboard
polls the API once per second to stay current.
| Project | Stack | Role |
|---|---|---|
HamStats.Website |
ASP.NET Core 10 | UDP ingestion, /api/v0/* API, serves the SPA |
HamStats.Data |
EF Core 10 + SQLite | Database context and entity models |
HamStats.Vue |
Vue 3 + Vuetify 3 + Vite | Real-time dashboard frontend |
- .NET 10 SDK
- Node.js (for the Vue frontend)
- Docker (only to run the published container — not needed for local development)
The published image bundles the backend, the built dashboard, and an embedded SQLite database.
Copy docker-compose.yml and .env.example from the repo, then:
cp .env.example .env
# Optional: edit .env to change ports
docker compose up -dThe dashboard is then available at http://localhost and N1MM+ broadcasts are received on UDP
port 12060. Point N1MM+ at this host's IP / port 12060 and contacts will appear as they're
logged. The SQLite database is persisted in a data/ directory alongside the compose file.
The
stabletag always points to the latest release. SetVERSIONin.envto pin a version.
No database server is required — the backend creates a local hamstats.db SQLite file on startup.
-
Run the backend (serves on
http://localhost:5000, OpenAPI doc at/openapi/v1.json):dotnet run --project HamStats.Website
-
Run the frontend (dev server on
http://localhost:3001, proxies/apito the backend):cd HamStats.Vue npm install npm run dev -
Point N1MM+ at it: configure N1MM+ to broadcast over UDP to this machine's IP on port
12060(N1MM+'s default broadcast port). Contacts and scores will appear on the dashboard as they're logged.
- Database connection —
HamStats.Website/appsettings.json(ConnectionStrings:Default); defaults to a localhamstats.dbfile, overridden to/data/hamstats.dbin the container. - UDP listen port —
12060(N1MM+ default); override withN1MM:BroadcastPortinappsettings.json. - Backend URL —
http://localhost:5000, set inappsettings.json(Kestrel).
The database is dropped and recreated on every startup — all data is cleared each time the backend restarts. This is intentional during early development for these ephemeral contest events. Schema changes are made by editing the model classes directly; EF migrations will be adopted once early development settles down.
docker build -t hamstats:local \
--build-arg version=0.0.1 \
--build-arg gitsha=$(git rev-parse HEAD) .dotnet publish HamStats.sln # backend
cd HamStats.Vue && npm run build # frontend → HamStats.Vue/build/