Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
smoke:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Syntax check
run: npm run check:syntax

- name: Unit tests
run: npm test
35 changes: 35 additions & 0 deletions demo/sample-calls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[
{
"id": 1,
"talk_group_id": "1001",
"timestamp": 1779033180,
"transcription": "Engine 12 responding to a medical call near Main Street and Oak Avenue.",
"audio_file_path": "",
"address": "Main Street and Oak Avenue",
"lat": 39.083997,
"lon": -77.152758,
"category": "Medical Call"
},
{
"id": 2,
"talk_group_id": "2001",
"timestamp": 1779033360,
"transcription": "Units checking a vehicle collision near the northbound ramp.",
"audio_file_path": "",
"address": "Northbound ramp",
"lat": 39.099721,
"lon": -77.184516,
"category": "Vehicle Collision"
},
{
"id": 3,
"talk_group_id": "3001",
"timestamp": 1779033540,
"transcription": "Police responding for a disturbance at the shopping center.",
"audio_file_path": "",
"address": "Shopping center",
"lat": 39.045753,
"lon": -77.118741,
"category": "Disturbance"
}
]
58 changes: 58 additions & 0 deletions docs/modernization-roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Scanner Map Modernization Roadmap

This roadmap breaks the larger architecture work into reviewable PRs. Each phase should preserve current behavior while creating room for deeper changes.

## Phase 1: Foundations

- Add shared config parsing and validation.
- Add a migration module that can replace scattered table creation over time.
- Add ingestion normalization helpers for SDRTrunk, TrunkRecorder, and rdio-scanner compatible uploads.
- Add a local demo data generator.
- Add smoke tests and CI.
- Add role and permission primitives that can back future RBAC.

## Phase 2: Runtime Integration

- Replace scattered `process.env` reads in `bot.js`, `webserver.js`, and `geocoding.js` with the shared config module.
- Move database initialization to the migration runner.
- Route upload handling through the ingestion normalization helpers.
- Keep the old endpoint behavior intact while shrinking request-handler complexity.

## Phase 3: Reliable Processing Queue

- Persist call processing jobs in SQLite or a dedicated queue backend.
- Track job state: pending, processing, failed, complete, and retryable.
- Retry transcription, geocoding, categorization, Discord publishing, and storage steps independently.
- Add admin visibility for queue depth, failed jobs, and processing latency.

## Phase 4: Local Demo And Developer Mode

- Add a demo server mode that serves sample calls without SDRTrunk, TrunkRecorder, Discord, geocoding keys, or audio hardware.
- Add sample talkgroups, categories, and map markers.
- Make frontend work possible with one command.

## Phase 5: Frontend Modules

- Split `public/app.js` into modules for map setup, markers, audio playback, live feed, auth, talkgroup modal, purge modal, and geocoding search.
- Gate verbose browser logging behind a debug flag.
- Add targeted browser smoke tests once the local demo mode exists.

## Phase 6: Data Model And Retention

- Add schema versioning and repeatable migrations.
- Add indexes for common call history, talkgroup, timestamp, and category queries.
- Add configurable retention rules for calls and audio.
- Add database maintenance docs for long-running deployments.

## Phase 7: Roles And Permissions

- Add a `role` column for users and migrate existing admin users.
- Replace ad hoc admin checks with permission checks.
- Introduce viewer, editor, moderator, and admin roles.
- Add UI controls only when the current user has the matching permission.

## Phase 8: Adapter Architecture

- Formalize ingestion adapters for SDRTrunk, TrunkRecorder, and rdio-scanner compatible uploads.
- Add adapter tests with real-world fixture payloads.
- Make future upload sources additive instead of route-handler rewrites.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
"scripts": {
"start": "node bot.js",
"web": "node webserver.js",
"import-talkgroups": "node import_csv.js"
"import-talkgroups": "node import_csv.js",
"check:config": "node scripts/check-config.js",
"check:syntax": "node --check bot.js && node --check webserver.js && node --check geocoding.js && node --check import_csv.js && node --check public/app.js",
"demo:data": "node scripts/generate-demo-data.js",
"test": "node --test test/*.test.js"
},
"engines": {
"node": ">=18"
Expand Down
20 changes: 20 additions & 0 deletions scripts/check-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
try {
require('dotenv').config();
} catch {
// Allows this checker to run before npm install; CI still installs dependencies.
}

const { loadConfig, redactConfig } = require('../src/config');

const result = loadConfig(process.env);

if (!result.isValid) {
console.error('Configuration validation failed:');
for (const error of result.errors) {
console.error(`- ${error.key}: ${error.message}`);
}
process.exit(1);
}

console.log('Configuration looks valid.');
console.log(JSON.stringify(redactConfig(result.config), null, 2));
46 changes: 46 additions & 0 deletions scripts/generate-demo-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');

const outputDir = path.join(__dirname, '..', 'data');
const outputFile = path.join(outputDir, 'demo-calls.json');

const now = Math.floor(Date.now() / 1000);
const calls = [
{
id: 1,
talk_group_id: '1001',
timestamp: now - 420,
transcription: 'Engine 12 responding to a medical call near Main Street and Oak Avenue.',
audio_file_path: '',
address: 'Main Street and Oak Avenue',
lat: 39.083997,
lon: -77.152758,
category: 'Medical Call'
},
{
id: 2,
talk_group_id: '2001',
timestamp: now - 240,
transcription: 'Units checking a vehicle collision near the northbound ramp.',
audio_file_path: '',
address: 'Northbound ramp',
lat: 39.099721,
lon: -77.184516,
category: 'Vehicle Collision'
},
{
id: 3,
talk_group_id: '3001',
timestamp: now - 60,
transcription: 'Police responding for a disturbance at the shopping center.',
audio_file_path: '',
address: 'Shopping center',
lat: 39.045753,
lon: -77.118741,
category: 'Disturbance'
}
];

fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputFile, `${JSON.stringify(calls, null, 2)}\n`);
console.log(`Wrote ${calls.length} demo calls to ${outputFile}`);
Loading
Loading