A fast, modern web map for Freifunk mesh networks. Built as a single Go binary with zero external dependencies — embeds all web assets and serves everything from one process.
- Single binary — zero runtime dependencies, embeds all frontend assets
- Federation mode — auto-discovers all Freifunk communities from api.freifunk.net
- Real-time updates via Server-Sent Events (SSE)
- Grafana integration — auto-discovers Grafana dashboards and renders per-node charts
- Fast startup — caches federation state for instant restarts
- Responsive dark-themed UI with Leaflet maps
- Node details with firmware, uptime, traffic charts, neighbour mesh view
- Community filtering with metacommunity grouping
- Search by hostname, node ID, or model
- Device deprecation warnings for end-of-life hardware
- No external CDN — all vendor assets (Leaflet, uPlot) are bundled
- Privacy-friendly — the only external request is to tile servers for the map background (FFMUC tiles by default, OpenStreetMap as fallback)
# Clone and build
git clone https://github.com/freifunkMUC/freifunk-map-modern.git
cd freifunk-map-modern
make build
# Single community mode (e.g. FFMUC)
cp config.example.json config.json
# Edit config.json with your community's data URL
./freifunk-map config.json
# Federation mode (all German Freifunk communities)
./freifunk-map config.federation.jsondocker build -t freifunk-map .
docker run -p 8080:8080 freifunk-map
# With custom config
docker run -p 8080:8080 -v ./config.json:/config.json freifunk-mapCopy config.example.json and adjust for your community:
To show all Freifunk communities on a single map, use federation mode:
{
"federation": true,
"refreshInterval": "120s"
}This auto-discovers communities from the Freifunk API, probes their data sources, discovers Grafana dashboards, and merges all node data into a unified map. Discovery state is cached to disk for instant restarts.
See config.federation.json for a ready-to-use federation config.
| Key | Type | Default | Description |
|---|---|---|---|
listen |
string | ":8080" |
HTTP listen address |
siteName |
string | "Freifunk Map" |
Site title |
dataURL |
string | required* | meshviewer.json URL |
refreshInterval |
string | "60s" |
Data refresh interval |
federation |
bool | false |
Enable federation mode |
grafanaURL |
string | Grafana base URL for charts | |
grafanaDashboard |
string | Dashboard URL template with {NODE_ID} |
|
mapCenter |
[lat, lng] | [48.13, 11.58] |
Default map center |
mapZoom |
int | 10 |
Default zoom level |
tileLayers |
array | Map tile layer definitions | |
domainNames |
object | Domain key → display name | |
links |
array | Header navigation links | |
devicePictureURL |
string | Device image URL template with {MODEL} |
|
eolInfoURL |
string | Link for end-of-life device warnings |
* Not required when federation: true
| Endpoint | Description |
|---|---|
GET /api/nodes |
All nodes (JSON array) |
GET /api/nodes/{id} |
Single node with neighbour details |
GET /api/links |
All mesh links |
GET /api/stats |
Aggregate statistics |
GET /api/config |
Client configuration (public, no secrets) |
GET /api/events |
SSE stream for real-time updates |
GET /api/communities |
Discovered communities (federation mode) |
GET /api/metrics/{id} |
Grafana time-series data for a node |
The map supports these data formats:
- meshviewer.json — the standard Gluon/BATMAN meshviewer format (preferred)
- nodelist.json — simpler format used by some communities as fallback
In federation mode, it also handles communities with:
- Non-standard technical types (
ffmap,hopglass) - Nodelist endpoints without
.jsonextension - Data URLs at non-standard paths (discovered via meshviewer
config.json)
.
├── main.go # Entrypoint + web embed
├── internal/
│ ├── config/config.go # Configuration types + loading
│ ├── store/store.go # Node store, snapshot, diff engine
│ ├── sse/sse.go # Server-Sent Events hub
│ ├── federation/
│ │ ├── discover.go # Community discovery + nodelist parsing
│ │ ├── grafana.go # Grafana auto-discovery + cache
│ │ └── store.go # Federation store + state persistence
│ └── api/handlers.go # HTTP API handlers + gzip middleware
├── web/
│ ├── index.html # Single-page app shell
│ ├── app.js # Frontend application
│ ├── app.css # Styles
│ └── vendor/ # Bundled Leaflet, uPlot, MarkerCluster
├── config.example.json # Single-community example config
├── config.federation.json # Federation mode config
├── Dockerfile # Multi-stage Docker build
├── Makefile # Build targets
└── go.mod # Go module (zero dependencies)
make build # Build for current platform
make dev # Run with go run
make release # Cross-compile for linux/amd64, linux/arm64, darwin/arm64
make docker # Build Docker imageContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Ensure
go build ./...passes - Submit a pull request
This project is licensed under the GNU Affero General Public License v3.0.
{ "listen": ":8080", "siteName": "My Freifunk Map", "dataURL": "https://map.example.net/data/meshviewer.json", "refreshInterval": "60s", // Optional: Grafana integration "grafanaURL": "https://stats.example.net", "grafanaDashboard": "/d/abc123/node?var-nodeid={NODE_ID}", // Map defaults "mapCenter": [52.52, 13.405], "mapZoom": 10, // Tile layers (at least one required) "tileLayers": [ { "name": "FFMUC Tiles", "url": "https://tiles.ext.ffmuc.net/osm/{z}/{x}/{y}.png", "attribution": "© OpenStreetMap contributors", "maxZoom": 19 } ], // Optional: domain name mapping "domainNames": { "my_domain_01": "Domain North", "my_domain_02": "Domain South" }, // Optional: header links "links": [ { "title": "Website", "href": "https://example.freifunk.net" } ], // Optional: device picture URL template "devicePictureURL": "https://map.aachen.freifunk.net/pictures-svg/{MODEL}.svg", // Optional: link for EOL device warnings "eolInfoURL": "https://example.freifunk.net/router-erneuern" }