tr-dashboard

May 26, 2026 · View on GitHub

Modern, responsive frontend for tr-engine radio scanning backend.

Live Demo — connected to a live MARCS (Ohio) trunk-recorder system.

Built with significant assistance from Claude Code (Anthropic). See commit history for details.

Screenshots

Dashboard — Live Monitoring

Real-time call activity with transcription previews, recorder status, decode rates, and active talkgroups.

Dashboard

Call History

Browse the full call history with filters, transcription previews, and inline playback.

Calls

Full-text search across all radio transcriptions with keyword highlighting, time/talkgroup filtering, and relevance ranking.

Transcription Search

Talkgroup Analytics

30-day call activity charts, hourly breakdowns, top units with call counts, keyword extraction from transcriptions, and unit cross-reference.

Talkgroup Analytics

Talkgroups

Browse all talkgroups with call stats, tag-based filtering, monitor/favorite controls, and color customization.

Talkgroups

Units

All known radio units with real-time event tracking, talkgroup affiliations, and activity indicators.

Units

Call Detail

Full call metadata, signal quality, transcription with word-level timing, and transmission breakdown.

Call Detail

Features

  • Real-time monitoring — Live call activity, active talkgroups, unit events, recorder status, system health
  • Historical analysis — Searchable call history, playback, filtering, and data exploration
  • Talkgroup Analytics — 30-day call activity charts, top units, keyword extraction from transcriptions, unit cross-reference
  • Transcription search — Full-text search across radio transcriptions with talkgroup/time filtering
  • Audio player — Global player with transmission timeline and keyboard shortcuts
  • Command palette — Quick navigation with Ctrl+K
  • Go To menu — Press G for quick navigation with search
  • Live monitoring — Auto-play calls from selected talkgroups
  • Talkgroup customization — Configurable color rules with hide/highlight modes and wildcard matching
  • Transcription display — View call transcriptions with word-level timing

Tech Stack

  • React 19 + TypeScript (strict mode)
  • Vite 7
  • Tailwind CSS v4
  • shadcn/ui (Radix-based components)
  • Zustand v5 (state management with persist middleware)
  • React Router v7
  • OpenAPI TypeScript (auto-generated API types)

Quick Start (Docker)

The Docker image serves static files only (via serve on port 3000). You need a reverse proxy (Caddy, Traefik, nginx) to route API requests to tr-engine. Full-stack example configs are in examples/.

Architecture

Browser → Reverse Proxy (Caddy/Traefik/nginx)
              ├── /api/*, /audio/*, /health/*  →  tr-engine:8080
              └── everything else              →  tr-dashboard:3000

The easiest way to get started. Caddy handles TLS automatically.

cp examples/.env.example .env        # edit with your values
docker compose -f examples/docker-compose.caddy.yml up -d

This starts Caddy, tr-engine, PostgreSQL, and tr-dashboard together. See examples/docker-compose.caddy.yml and examples/Caddyfile.

Option B: Full Stack with Traefik

If you already run Traefik:

cp examples/.env.example .env        # edit with your values
docker compose -f examples/docker-compose.traefik.yml up -d

See examples/docker-compose.traefik.yml.

Option C: Dashboard Only

If tr-engine is already running and you have your own reverse proxy:

services:
  tr-dashboard:
    image: ghcr.io/trunk-reporter/tr-dashboard:latest
    ports:
      - "3000:3000"

Then configure your proxy to route /api/*, /audio/*, and /health/* to tr-engine and everything else to tr-dashboard:3000. The key requirement is that /api/events (SSE) must have flush_interval -1 or equivalent to avoid buffering.

Migrating from v0.9.x (Caddy-based image)

Previous versions bundled Caddy inside the Docker image and accepted TR_ENGINE_URL, TR_AUTH_TOKEN, and SITE_ADDRESS environment variables. Starting with v0.10.0:

Before (v0.9.x)After (v0.10.0+)
Caddy bundled in imageStatic-only image, bring your own proxy
Port 80/443Port 3000
TR_ENGINE_URL env varProxy config routes /api/* to tr-engine
TR_AUTH_TOKEN injected by CaddyJWT auth via login page, or proxy-injected header
SITE_ADDRESS for auto-HTTPSConfigured in your proxy (Caddy/Traefik)

To migrate: Use one of the full-stack examples above (examples/docker-compose.caddy.yml for the closest equivalent to the old setup) or add tr-dashboard to your existing proxy config. If you were using TR_AUTH_TOKEN, configure your Caddy/nginx to inject the header — see Reverse proxy setup.

Build from Source

git clone https://github.com/trunk-reporter/tr-dashboard.git
cd tr-dashboard
docker build -t tr-dashboard .

Development Setup

For contributing or local development.

Prerequisites

Install

git clone https://github.com/trunk-reporter/tr-dashboard.git
cd tr-dashboard
npm install

Configure

The dev server proxies API requests to tr-engine. If your backend requires authentication, create a .env file:

# .env
TR_AUTH_TOKEN=your-auth-token-here

The proxy target is configured in vite.config.ts (default: https://tr-engine.luxprimatech.com). Change this to point at your own tr-engine instance.

Run

npm run dev

Runs on http://localhost:5173 with API proxy to the tr-engine backend.

Build

npm run build        # Type-check + build
npm run lint         # Type-check only
npm run api:generate # Regenerate API types from OpenAPI spec

npm run api:generate expects tr-dashboard and tr-engine to be checked out as sibling directories and reads the backend spec from ../tr-engine/openapi.yaml.

Before marking implementation work complete, follow the project quality gates in docs/quality-gates.md.

Write Access

tr-dashboard supports inline editing of talkgroup metadata (name, group, tag, priority, description) and unit names directly from their detail pages.

How it works

  • If your tr-engine has no WRITE_TOKEN configured, editing works for everyone automatically.
  • If WRITE_TOKEN is set, users need to enter it in Settings → Write Access to enable editing. The token is stored in the browser's localStorage.
  • Without a valid write token, the Edit button still appears but saves will show an error message directing users to Settings.

Reverse proxy setup

If you run tr-dashboard behind a reverse proxy that injects an auth token, make sure it doesn't overwrite the Authorization header when the browser sends one. The dashboard sends the write token as Authorization: Bearer <token> on write requests (PATCH/POST/PUT/DELETE).

Caddy example — use conditional injection so the read token is only added when the browser doesn't send its own:

handle /api/* {
    @no_auth not header Authorization *
    request_header @no_auth Authorization "Bearer {$TR_AUTH_TOKEN}"
    reverse_proxy tr-engine:8000
}

Nginx example:

location /api/ {
    # Only set auth header if not provided by the browser
    set $auth "Bearer your-read-token";
    if ($http_authorization) {
        set $auth $http_authorization;
    }
    proxy_set_header Authorization $auth;
    proxy_pass http://tr-engine:8000;
}

Keyboard Shortcuts

KeyAction
Ctrl+KCommand palette
GOpen Go To menu
G then DGo to Dashboard
G then CGo to Calls
G then TGo to Talkgroups
G then UGo to Units
G then SGo to Settings
[Toggle sidebar

Audio Player

KeyAction
SpacePlay/pause
JNext call
KPrevious call
LSeek forward 5s
HSeek backward 5s
RReplay current call
MMute/unmute

Roadmap

See the Trunk Reporter Roadmap for the cross-repo project tracker with priorities and phases.

License

MIT