No description
Find a file
2026-08-04 14:51:34 +02:00
backend Fix ntfy bearer token usage 2026-08-04 14:51:34 +02:00
frontend Add test push button in admin area 2026-08-04 14:43:08 +02:00
.air.toml Split backend/frontend and refactor 2026-02-19 17:00:59 +01:00
.env.example Add admin token env var to compose 2026-02-10 11:05:42 +01:00
.gitignore Split backend/frontend and refactor 2026-02-19 17:00:59 +01:00
compose.dev.yml Fix ntfy bearer token usage 2026-08-04 14:51:34 +02:00
compose.test.yml Split backend/frontend and refactor 2026-02-19 17:00:59 +01:00
compose.yml Fix ntfy bearer token usage 2026-08-04 14:51:34 +02:00
Dockerfile Split backend/frontend and refactor 2026-02-19 17:00:59 +01:00
Dockerfile.dev Split backend/frontend and refactor 2026-02-19 17:00:59 +01:00
LICENSE Update URLs and fix copyright notice 2026-07-21 15:21:01 +02:00
README.md Fix ntfy bearer token usage 2026-08-04 14:51:34 +02:00

DMRmap

A web app that visualizes DMR repeaters on an interactive map. Filter by band and network, search by callsign or address, find repeaters along a driving route, and export CPS-ready channel configurations.

Live: https://dmrmap.de

Features

  • Interactive OpenStreetMap with repeater markers (blue = VHF/2m, red = UHF/70cm)
  • Band filtering (2m / 70cm) and network filtering (BrandMeister, DMR+, TGIF, Other)
  • Toggle visibility of personal hotspots and inactive repeaters
  • Free-text search by callsign, city, or DMR ID
  • Address-to-address routing via OSRM with adjustable corridor width
  • Click-to-pin with adjustable radius search
  • Detailed repeater popups (frequencies, color code, timeslots, hardware, power, antenna height, BrandMeister status)
  • Real-time Last Heard heatmap via BrandMeister WebSocket
  • User reports for incorrect repeater data (offline, wrong frequency, changed callsign, other), reviewable in the admin area with optional ntfy push notifications
  • CPS Studio: manage talk groups, set timeslots, export Motorola CPS 2.0 compatible XML
  • Coordinate display with Maidenhead grid locator
  • Dark mode (follows system preference)
  • Internationalization (English, German, Spanish, French, Italian, Polish)
  • No external CDN dependencies — all assets served locally

Tech Stack

  • Backend: Go (stdlib net/http), PostgreSQL 17 via pgx/v5, migrations via goose/v3
  • Frontend: TypeScript (bundled with esbuild), Leaflet, i18next
  • Testing: Vitest (115 unit tests)
  • Infrastructure: Docker Compose, multi-stage Dockerfile

External Services

Service Usage Auth
RadioID Repeater database (seeded from rptrs.json dump) None
BrandMeister API Device status sync (/v2/device/{id}), talk group registry (/v2/talkgroup) None (public read)
BrandMeister WebSocket Real-time Last Heard feed for heatmap via socket.io None
Nominatim Address geocoding and autocomplete (client-side) None
OSRM Driving route calculation (client-side) None
ntfy Push notification when a user report is submitted (optional) Access token (bearer)

The BrandMeister talk group registry is fetched once at Docker build time and bundled as frontend/static/talkgroups.json. It is used for talk group name resolution and autocomplete in CPS Studio.

Running with Docker

docker compose up --build

Open http://localhost:8080. PostgreSQL data is persisted in a named volume.

Development

Hot-reload with air (Go) and esbuild watch (TypeScript):

docker compose -f compose.dev.yml up --build

The dev container mounts the project directory, so changes to Go and frontend files are picked up automatically.

Running Tests

docker compose -f compose.test.yml run --rm test

Environment Variables

Variable Default Description
DATABASE_URL postgres://dmrmap:dmrmap@localhost:5432/dmrmap?sslmode=disable PostgreSQL connection string
JSON_PATH rptrs.json Path to RadioID repeater dump
BMRPTRS_PATH bmrptrs.json Path to BrandMeister repeater dump
LISTEN_ADDR :8080 HTTP listen address
STATIC_DIR static Path to static frontend assets
MIGRATIONS_DIR migrations Path to SQL migration files
BM_SYNC (disabled) Set to true to enable periodic BrandMeister device sync
ADMIN_TOKEN (disabled) Set to enable the admin interface at /admin/
NTFY_URL (disabled) ntfy server base URL (e.g. https://ntfy.example.com). Together with NTFY_TOPIC it enables push notifications for new user reports
NTFY_TOPIC (disabled) ntfy topic that report notifications are published to
NTFY_TOKEN (none) ntfy access token, sent as Authorization: Bearer. Required for protected topics
PUBLIC_BASE_URL (none) Public base URL of the site (e.g. https://dmrmap.de). Used to add a click-through link to the admin view in ntfy notifications

Both NTFY_URL and NTFY_TOPIC must be set for notifications; otherwise reports are stored but nothing is pushed. On startup the app logs either ntfy notifications enabled: <topic URL> (auth: ...) or ntfy notifications disabled. The Test push button in the admin Reports view publishes a test notification and shows the upstream error if it fails.

API Endpoints

Method Path Description
GET /api/repeaters Repeaters within map bounding box (query: minLat, maxLat, minLng, maxLng, band, network, hotspots, inactive)
GET /api/repeaters/radius Repeaters within radius of a point (query: lat, lng, radius)
POST /api/repeaters/route Repeaters along a route corridor (body: GeoJSON coordinates + corridor width)
GET /api/repeater Single repeater by ID (query: id)
GET /api/repeaters/search Free-text search (query: q)
POST /api/reports Submit a user report about a repeater (body: repeater_id, report_typeoffline, frequency, callsign or other — and message, max 300 characters). Rate limited to 5 reports per IP per hour

Project Structure

├── backend/
│   ├── main.go          # Route registration, env config
│   ├── db.go            # DB queries, Repeater struct, geo helpers
│   ├── handlers.go      # Public API handlers
│   ├── admin.go         # Admin auth middleware + admin API
│   ├── reports.go       # User report API (public + admin) and rate limiting
│   ├── ntfy.go          # ntfy push notifications for new reports
│   ├── bmdevice.go      # BrandMeister device sync
│   ├── seed.go          # Database seeding from JSON dumps
│   ├── migrate.go       # Goose migration runner
│   ├── go.mod / go.sum
│   ├── migrations/      # SQL migrations (goose)
│   └── data/            # RadioID + BrandMeister JSON dumps
├── frontend/
│   ├── src/             # TypeScript source + tests
│   ├── static/
│   │   ├── index.html   # Single-page HTML shell
│   │   ├── admin.html   # Admin interface
│   │   ├── style.css    # Styles with dark mode support
│   │   ├── i18n.js      # i18next initialization
│   │   ├── locales/     # Translation files (en, de, es, fr, it, pl)
│   │   └── lib/         # Vendored JS libraries (Leaflet, i18next, socket.io)
│   ├── package.json
│   ├── tsconfig.json
│   └── vitest.config.ts
├── Dockerfile           # Production multi-stage build
├── Dockerfile.dev       # Development image with air + esbuild
├── compose.yml          # Production Docker Compose
├── compose.dev.yml      # Development Docker Compose
└── compose.test.yml     # Test runner Docker Compose

Data

Repeater data provided by RadioID. Availability and talk group data by BrandMeister. This project is not affiliated with or endorsed by RadioID or BrandMeister.

License

MIT