Add D-STAR presence alerts (QuadNet/ircDDB source) and local dev mode - #29
Add D-STAR presence alerts (QuadNet/ircDDB source) and local dev mode#29jbburns wants to merge 17 commits into
Conversation
Adds dstar.js, a receiver that tails the public QuadNet heard log and the ircDDB live log, classifies transmissions into "active" (voice) and "linked" (link command) events, drops info/echo/unlink control traffic, and deduplicates per callsign, node, reflector and event type. Spots carry source/mode "dstar", no frequency, and new fields dvEvent, dvNode and dvReflector. The matcher gets matching conditions (node and reflector match with or without the module letter), the band range is only derived when a frequency exists, and notification params include the new fields. tools/dstarTest.js runs the receiver against the live feeds or a saved log without a database. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rJsJxn47T1Bff6xi6g7ab
…imulator Node and reflector identifiers are now "W4HFH-C" / "REF030-C" instead of space-separated, so they survive whitespace splitting in trigger lists, URL parameters and telnet output. The simulator accepts D-STAR spots (dvEvent, dvNode, dvReflector) without a frequency, and notification params include dvSuffix and dvDuration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rJsJxn47T1Bff6xi6g7ab
The telnet "DX de" formatter threw on spots without a frequency or spotter, and the exception was swallowed silently, so D-STAR telnet alerts were dropped. Use "DV" in place of the frequency, tolerate a missing spotter, include event/node/reflector in the comment, and log notify failures. The app notifier is now skipped when APNS/FCM config is absent so the server can start without push credentials. Adds LOCAL_DEV.md and tools/seedLocalUser.js for running the stack against a local MongoDB and Redis. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rJsJxn47T1Bff6xi6g7ab
All modules now load their config through config_loader.js, which requires ./config.js by default or the file named by HAMALERT_CONFIG. config-local.js is a committed, credential-free config (local Mongo and Redis on non-default ports, no push, no RBN/cluster logins, no Club Log key). tools/localDev.js starts clean mongo:7 and redis:7 containers, waits for them, seeds the test user and D-STAR triggers, optionally builds and runs the hamalert-web dev image, prints how to connect, and runs the server; containers are removed on exit unless --keep is given. Club Log lookups are skipped when no API key is set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rJsJxn47T1Bff6xi6g7ab
Podman (docker CLI emulation) refuses short names like "mongo:7" unless unqualified-search registries are configured; docker.io/library/... works on both Docker and Podman. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
composer install inside the web container was rewriting the tracked vendor/ files in the bind-mounted hamalert-web checkout. Mount the hamalert-dev-web-vendor volume over vendor/ instead, so packages are installed once and the checkout stays clean; --down removes the volume. Note that Podman with the podman-docker shim works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
So the mobile app's feed (which only returns spots matched by an "app" trigger) has something to show when the app runs in browser dev mode against the local stack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dstar_nodes.js keeps a directory of repeater modules and their registered frequencies from the QuadNet (gateway.php) and ircDDB (repeater.php) lists, refreshed hourly and persisted like the Club Log cache. normalizeSpot looks up dvNode there and sets the frequency, from which the band derives as for any other spot. If the node is unlisted the band is guessed from the module letter (A 23cm, B 70cm, C 2m) and flagged bandIsGuessed; failing that band is "unknown". Every D-STAR spot now has a spotter (the node's callsign when no gateway was reported). The feed poll interval defaults to 15 s instead of 5 s. tools/dstarNodeTest.js exercises the directory without a database. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
…limiting
D-STAR presence spots share one frequency per hotspot, so the per-callsign
frequency/mode and band/mode limiters were collapsing distinct events (e.g.
"linked to REF058-E" right after "linked to REF030-C", or "active" right
after "linked" on the same reflector). Those two limiters now also require
the D-STAR event+reflector/node identity to match. Also fixes checkLimit's
simulated-spot shortcut to return the same {limitExceeded, generalLimitExceeded}
shape callers expect, and adds a documented, default-off config.rateLimit.disabled
flag (on only in config-local.js) so npm run local-dev never rate-limits alerts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
…ivity) QuadNet and ircDDB only ever see repeater/hotspot traffic, never DPlus/REF reflector activity — zero overlap measured against dstarusers.org's "last heard" page. Poll it every 30s, diff against a watermark of previously-seen rows, and emit an "active" spot per row; hotspot/dongle users are reported by the reflector with no module, so runMatcher() expands a module-less dvReflector condition to match every module letter. Also documents the feed in README.md/LOCAL_DEV.md and adds a usage line for HAMALERT_CONFIG to tools/dstarTest.js's help text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
…flector-module rows
The dedupe key included the repeater node and the reflector module, so the same
transmission reported by QuadNet ("REF030-C via N4EDO-B") and by dstarusers.org
(module row "REF030-C" or dongle row "REF030", both without a node) produced two
alerts; the rate limiter's D-STAR key had the same flaw. Both now key on the
reflector callsign without its module (or the node when there is no reflector).
A dstarusers.org reflector-module row's band text ("REF030 C 2 Meters") is just
the module's label and says nothing about the user's RF band, so it is no longer
used; only repeater/gateway rows ("NS9RC B 440 MHz") carry a band.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
At startup the feeds prime the dedupe cache with up to an hour of old records, each getting a full dedupeInterval from "now", so a station heard 40 minutes before a restart stayed silent for 15 minutes after it. Entries now live for dedupeInterval minus the record's age, and records older than the window are not cached at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
config.dstar.dedupeInterval = 0 now means no suppression at all; config-local.js uses it so every test transmission alerts (production keeps 15 minutes). Records already in the logs at startup are still only used for priming. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
…ongle rows
dstarusers.org's dongle/hotspot login rows (e.g. "REF030 Dongle User") are DPlus logins
reported without a module, not transmissions; when the same user transmits, a proper module
row follows. These are now dropped entirely at the parser (parseDstarusersNode/parseDstarusersRow)
instead of being alerted on as a module-less reflector report, so runMatcher() no longer needs to
expand a bare reflector into every possible module and normalizeSpot()/emitEvent() no longer treat
"no module" as a case to special-case.
The dashboard-scraping module resolver added in e0e9827 is reverted: reflector dashboards can't
reliably supply the module for a merely-listening user (differ per reflector, some don't even
serve the data anymore), and dropping the module-less rows makes the enrichment moot anyway.
Spots now report source as the feed that produced them ('quadnet', 'ircddb' or 'dstarusers')
instead of a single 'dstar' source; mode stays 'dstar' for all three so triggers and the UI can
still treat D-STAR as one thing. dvFeed is removed (redundant with source). Also: dstarusers.org's
Location column is no longer copied into the comment for reflector-module rows (it names the
reflector's own town, not the operator's); repeater rows keep it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
Zero meant the same transmission reported by QuadNet and dstarusers.org alerted twice; two minutes collapses those (their timestamps differ by up to a minute) while still letting a repeated test alert again quickly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEzvaE4xxhsuShqGetmVYb
Update: third feed, rate limiting, dedupe, per-feed sourcesTesting against live REF030/REF058 traffic showed that QuadNet and ircDDB never see DPlus/REF reflector activity: stations connected through hotspots and dongles report to neither network (zero callsign overlap between the two worlds in the same hour). This batch of commits addresses that and a number of things found along the way. New feed: dstarusers.org. Source names the feed. Spots now have Dedupe. One alert per callsign, event and place (reflector without module, else node) per Rate limiter. The per-callsign+frequency/band+mode limits now also key on the D-STAR event and place, so "linked to REF030-C" no longer suppresses "active on REF058-E" from the same hotspot (all its events resolve to one frequency). New Smaller items. Reflector-module rows carry no band (the module label says nothing about the operator's RF band) and no location comment (it was the reflector's town); node-less spots get Tried and dropped. Resolving the module of dongle rows from the reflector's own dashboard: the dashboards differ per reflector (REF030 serves a static page over http and a socket.io app over https) and list dongle users as "listening" without a module until they transmit, at which point dstarusers.org reports the module anyway. Permission-request emails to QuadNet, ircDDB and dstarusers.org are drafted and will be sent before this goes live. |





Implements the D-STAR side of https://github.com/orgs/hamalert/discussions/17 (presence alerts for D-STAR: "callsign X is active on, or linked to, repeater/reflector module Y"), plus a one-command local development mode that made it testable.
What's new
D-STAR presence source (
dstar.js)openquad.net/ics/ics, HTTP Range tail) and ircDDB (live.ircddb.net:8080/jj3.yaws, line-number poll). Both feeds are normalized into one record shape and go through one shared dedupe cache, so a dual-homed gateway never produces two alerts.activeevent; a link command in UR (e.g.REF030CL) →linkedevent (and alsoactiveif held long enough to be voice, which is common); info/echo/unlink and other control commands never alert. Directed (callsign-routed) calls are ignored by default (ignoreDirectedCalls).dedupeInterval(15 min). Node/reflector identifiers areW4HFH-C/REF030-C; the module letter is optional in trigger conditions (REF030matches all modules).source: 'dstar',mode: 'dstar', new fieldsdvEvent,dvNode,dvReflector, plusdvSuffix,dvDuration,comment(TX message),spotter(reporting gateway, falling back to the node's callsign).dstar_nodes.js): the heard records carry the repeater module but no frequency, so the server keeps a directory of module → registered frequency built from QuadNet'sgateway.php(about 5100 modules) and ircDDB'srepeater.php(about 490 more), refreshed hourly and persisted like the Club Log cache.normalizeSpotlooks updvNodeand setsfrequency(then band derives as usual;frequencySource: 'nodelist'). In a live sample all 270 recent QuadNet nodes resolved. If a node is unlisted the band is guessed from the module letter (A 23cm, B 70cm, C 2m) and flaggedbandIsGuessed; failing thatbandis"unknown".npm run dstar-nodes -- <node>...exercises the directory.dvEvent,dvNode,dvReflector; notification params include the new fields; the simulator accepts D-STAR spots with or without a frequency (resolved from the directory when omitted).Local development mode
config_loader.js: every module loads config through it;./config.jsstays the default,HAMALERT_CONFIG=<file>overrides.config-local.js: committed, credential-free config (local Mongo/Redis on non-default ports, no push, no RBN/cluster logins).npm run local-dev(tools/localDev.js): starts cleanmongo:7andredis:7containers (Docker or Podman), seeds a test user + two D-STAR triggers, optionally builds and starts the hamalert-web dev image (hamalert/hamalert-web PR), prints how to connect, and runs the server. SeeLOCAL_DEV.md.Design notes / open points for review
VIS ON; QuadNet publishes everything, so HamAlert exposes nothing that isn't already public. TheCQCQCQ-only idea from the discussion doesn't distinguish a QSO on modern D-STAR (both sides keep UR=CQCQCQ through a reflector), so the filter is "voice vs. control traffic" instead; alert wording is "heard on", never "is talking to".minVoiceDuration,minVoiceDurationLinkCommand,dedupeInterval) are config values and will want tuning against real traffic.Testing
npm run local-dev(Docker and Podman): simulated D-STAR spots reach the telnet notifier, Mongo and the Redis stream; the module-lessREF030condition matchesREF030-A; live QuadNet/ircDDB spots flow through the pipeline.Screenshots
Terminal captures (server startup with both feeds priming, live D-STAR spots, telnet session receiving simulated alerts,
dstar-teston the live feed) are attached below.Companion PRs: hamalert/hamalert-web (trigger editor, docs, Alerts page, dev image) and hamalert/hamalert-app (rendering of D-STAR spots).
🤖 Generated with Claude Code