The analyzer drawer and path-map restore both read the live ?hash and fetch by hash directly, independent of whether the packet is in the loaded list. Deleting the param once the first page settles without a match unmounts the drawer and can drop path-map restores, for any shared link more than a couple of minutes old.
BEACON Web
Real-time LoRa mesh packet analyzer. Desktop-first, dark-mode-primary, dense information display for radio hobbyists.
Built with React 19, TypeScript, Tailwind CSS 4, TanStack Query, and TanStack Virtual.
Deployment
1. Copy the docker/ folder to your server
scp -r docker/ user@your-server:/opt/docker/beacon-web
2. Create a .env file
cd /opt/docker/beacon-web
cat > .env << 'EOF'
DOMAIN=dev.meshcore.ca
VITE_API_BASE=https://dev.meshcore.ca/api/v1
VITE_WS_URL=wss://dev.meshcore.ca/ws
EOF
| Variable | Description |
|---|---|
DOMAIN |
Domain for HTTPS (Caddy auto-provisions Let's Encrypt certs) |
VITE_API_BASE |
Backend REST API base URL |
VITE_WS_URL |
Backend WebSocket URL |
3. Start the services
docker compose up -d
The images are public on GitHub Container Registry — no docker login required.
If a pull fails with 403 Forbidden, the package visibility has regressed to
Private; a maintainer needs to set it back to Public (see the troubleshooting note
in beacon-docs).
Caddy will automatically obtain a TLS certificate for your domain. Ensure DNS is pointed at your server before starting.
Local Development
npm install
cp .env.example .env # edit with your backend URLs
npm run dev # starts Vite dev server at http://localhost:5173
Commands
| Command | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Type-check and build for production |
npm run preview |
Preview production build locally |
npm run lint |
Run ESLint |
npx vitest run |
Run tests |
npx tsc --noEmit |
Type-check without emitting |
Project Structure
docker/
docker-compose.yml # production deployment compose file
Caddyfile # internal Caddy config (static file serving)
Caddyfile.proxy # reverse proxy config (HTTPS termination)
docker-entrypoint.sh # runtime env var injection
Dockerfile # multi-stage build (Node + Caddy)
src/
api/
client.ts # typed REST client (fetch wrapper)
ws-manager.ts # WebSocket connection, reconnect, subscription management
components/ # shared UI components
features/ # feature modules (packets, nodes, channels, map, stats)
hooks/ # React hooks (region, theme, WebSocket)
lib/ # constants, formatters, theme utilities
types/ # TypeScript types and enums
App.tsx # providers + routing + WS init
main.tsx # entry point
index.css # Tailwind setup, theme tokens, animations
Architecture
- Region-driven: All data queries and WS subscriptions are scoped to an IATA region code. Changing region resets the cache and resubscribes.
- Live + historical merge: WebSocket pushes live packets into a
LivePacketStorebuffer (capped at 500). Historical data comes from cursor-paginated REST viauseInfiniteQuery(max 20 pages). Both are merged and deduped at render time. - Client-side filtering: Filters are not part of the query key. The cache holds all packets for the current region; filters are applied via
useMemo. Toggling a filter is instant with no refetch. - Reconnect with jitter: Exponential backoff with +/-25% random jitter prevents thundering herd on server bounce.
Contributing
See CONTRIBUTING.md. All contributors are welcome — please also read the Code of Conduct. To report a security issue, see SECURITY.md.
License
Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE for the full text and CONTRIBUTORS.md for acknowledgements.