Project: EarthRanger Web Client (das-web-react)
Domain: Conservation field operations management
- EarthRanger: Real-time operational platform for monitoring wildlife, assets, and field activities within protected areas.
- DAS (Domain Awareness System): Legacy product name. EarthRanger is the official name today. The repository (
das-web-react) and some API references still use thedasprefix. - Tenant: Each EarthRanger deployment serves a single conservation site or organization. The web client connects to exactly one tenant's backend. Multi-tenancy is server-side.
- Companion products:
- Ecoscope: advanced analytics layer.
- Gundi: middleware aggregation layer connecting external data sources to EarthRanger.
Events are time-stamped incident reports. Each event has a state (active / resolved), a priority (0 - none / 100 - low / 200 - medium / 300 - high), a reporter, optional notes and files, and an event_details object that holds the data the user entered in the event's detail form. They can be Points or Polygons on the map.
The codebase historically called events "reports". "Event" is the preferred term.
Event Types are the templates events belong to (e.g., "Injured Animal", "Snare"). They define the display name, icon, default priority, default state, geometry type, and a form schema, which is a JSON object that drives the event's detail form. Event types come in two versions: v1 (legacy, rendered with @rjsf/react-bootstrap) and v2 (current, rendered with a custom SchemaForm).
Event Type Categories group event types into a taxonomy (e.g., "Monitoring", "Security", "Logistics").
Incident Collections are events with is_collection: true that group multiple child events through a contains relationship (e.g., a snare, carcass, and arrest under one incident).
UI
- Events Sidebar (
/events): text search, filters (state, priority, event type, reporter, date range), and sorting. - Event Detail View (
/events/:idor/events/new): Details section (state, reported-by, priority, location, date/time, and schema-driven form fields), Activity section (notes, files, and contained events for incident collections), Links section (linked events and patrols), and History section (audit trail fromupdates). - Map: Point events render as clustered icon markers; polygon events as priority-colored fills.
- A heatmap overlay can be toggled from Map Layers → Events.
Subjects are monitored entities: collared animals, vehicles, aircraft, personnel, and fixed sensors. Each subject has a subject_type (e.g., wildlife, person, vehicle, static_sensor) and a subject_subtype that further classifies it (e.g., ranger, dugong, stationary-radio). Key fields include is_active, tracks_available, last_position (a GeoJSON Point Feature), and device_status_properties, an array of telemetry readings (label/units/value). Real-time position updates arrive via WebSocket.
Subjects are organized into subject groups, a named hierarchy with nested subgroups.
A source is a telemetry device (GPS collar, radio, acoustic sensor). Sources generate observations, discrete timestamped position records that each carry a source UUID and their own device_status_properties. The backend assigns sources to subjects for defined time windows, but the client always queries by subject ID.
Tracks are the ordered sequence of a subject's positions as a GeoJSON LineString:
track:FeatureCollectionofLineStringfeatures withcoordinateProperties.times(one timestamp per coordinate)points:FeatureCollectionofPointfeatures derived from the line, each with a computedbearingfor directional arrowsfetchedDateRange: the{ since, until }window already loaded
Tracks are lazy-loaded and cached. Track history depth is user-configurable (default 7 days) and can be locked to the active event filter's date range. When time-of-day coloring is enabled, a trackSegments FeatureCollection is added that segments the line into time-range buckets, each with a distinct color.
Track visibility cycles through three states: hidden (default), visible (line at lower opacity), and pinned (full opacity, higher render priority). Subjects can be included to the subject heatmap, a density surface overlay on the map.
UI
- Map Subjects Layer: renders subject icon markers from
last_position, clustered at lower zoom levels. - Map Tracks Layer: draws the line string as a colored line plus a symbol layer of rotated arrows at each timepoint. Default color is deterministic and seeded by subject ID; with time-of-day coloring, each segment uses a period-specific color.
- Map Layers Sidebar Tab (
/layers): searchable, sortable subject list (grouped hierarchy or flat). Controls: toggle map visibility, cycle track state (hidden → visible → pinned), toggle heatmap membership, jump to location, view history.
Patrols are timed field activities (foot, vehicle, aerial, etc.). Patrol types define the activity category and supply a default icon and priority.A patrol has a top-level state (API: open / done / cancelled), and optional objective, title, priority, notes, and files. It always contains a patrol segment, which holds:
patrol_type: type identifier referencing a patrol type objectleader: a subject reference (ranger, vehicle, aircraft) whose observation track becomes the patrol tracktime_range:{ start_time, end_time }, actual start/end timestampsscheduled_start/scheduled_end: planned times before the patrol is startedstart_location/end_location: explicit{ latitude, longitude }overrides for start/stop map markersevents: events linked to this segment
UI states are derived client-side from the API state and segment times, the API only knows open, done, cancelled:
| UI state | Condition |
|---|---|
scheduled |
scheduled_start is more than 1 hour in the future |
ready_to_start |
scheduled_start is within the next hour |
start_overdue |
scheduled_start is 30+ min past but patrol not yet started |
active |
start_time set and in the past, no end_time |
done |
end_time is in the past, or API state: 'done' |
cancelled |
API state: 'cancelled' |
invalid |
No patrol_segments, or no pending segment matches any known state |
Tracks. The patrol track is the leader subject's track, trimmed to the segment's time_range. Only active and done patrols can display tracks. Track visibility follows the same three-state cycle as subjects (hidden → visible → pinned).
UI
- Patrols Sidebar (
/patrols): Feed sorted ready_to_start → start_overdue → active → scheduled → done → cancelled. Filters: text search, date range, patrol type, tracked-by, and status. - Patrol Detail View (
/patrols/:idor/patrols/new): Plan section (leader/tracked-by, objective, start/end datetime and location, with auto-start/auto-end scheduling toggles), Activity section (linked events, notes, files), and History section (audit trail fromupdates). - Map: Active and done patrols with visible/pinned tracks show the leader's route as a colored line with start and stop icon markers at each endpoint. Patrol symbols are labeled with a ticker.
Spatial features are static geographic data layers displayed on the map alongside live operational data: zones, boundaries, infrastructure, habitat areas, and reference cartography. They are organized into featuresets, named collections grouped by feature type.
Feature geometry can be Point, LineString, or Polygon. Per-feature styling properties (fill, stroke, fill_opacity, stroke_opacity) are stored on the feature and applied via vector tiles (GET /spatialfeatures/tiles/{z}/{x}/{y}.pbf). Features may carry an analyzer_type (geofence or proximity) linking them to an analyzer definition.
UI
- Map Layers Sidebar - Features tab: featuresets listed hierarchically (featureset → type → individual feature). Per-feature visibility toggle and jump-to-bounds.
- Clicking a feature on the map opens a popup with its name and type.
Analyzers are server-side algorithms that evaluate streaming or recent data against configured rules. For example:
- Geofence: polygon boundary, triggers when a subject enters or exits.
- Proximity: point with a radius, triggers when a subject comes within range.
Some analyzers have one or more spatial groups, named GeoJSON feature collections. The web client renders these as dashed polygon and line overlays on the map (yellow for warning severity, red for critical). The client does not execute analyzers; it only displays their geographic boundaries.
Alerts: the notifications raised when an analyzer fires, are managed entirely server-side. The client provides a modal iframe in global the menu modal and in Settings → Alerts.
UI
- Map Layers Sidebar — Analyzers tab: list of analyzers with visibility toggles. Selecting an analyzer zooms the map to its bounds and shows a popup linking to its admin configuration page.
- Alerts modal: opened from the global menu; embeds the tenant's alert management page in an iframe.
The time slider is a map control that sets a virtual date, a specific historical moment, shifting the map's display to reflect subject positions and event states at that time.
EarthRanger works in WGS84 but the client supports multiple display and input formats (DEG, DMS, DDM, UTM, MGRS).
Users can add custom CRS definitions by EPSG code. DEG is always included as the baseline.
UI
- Settings → Map: manage stored CRS definitions and configure a bounding-box overlay for the active CRS.
- All fields or UI widgets that capture or display coordinates render values in the currently selected format.
Settings → Map organizes display controls in three groups:
- General: Lock map, enable 3D terrain, simplify map data at low zoom, and open Coordinate System Settings.
- Display: Show track timepoints along subject lines, show inactive radios, per-class clustering toggles with an optional cluster footprint polygon overlay.
- Map Markers: Show/hide name labels independently for subjects, stationary sensors, events, and patrols; toggle the user's own GPS location marker.
Messaging lets authorized users send and receive two-way messages with subjects that carry messaging-capable devices (e.g., inReach satellite communicators, supported radio integrations).
Messages are grouped by date and sender, support infinite-scroll pagination, and track read/unread status. Real-time updates arrive via WebSocket. The messaging UI is accessible from subject map popups and the nav.
Sound notifications for new inReach messages are configurable in Settings → General.
The sidebar is a vertical panel with tabs shown conditionally by system config flags and user permissions:
| Tab | Condition |
|---|---|
| Events | EVENTS flag |
| Patrols | PATROL_MANAGEMENT flag + patrol read permission |
| Gear | Gear data available |
| Map Layers | ANALYZERS, SPATIAL_FEATURES, SUBJECTS, or EVENTS flag |
| Settings | Always |
Settings has three sub-tabs:
- General
- App Refresh: Which UI state survives a page reload.
- Language: UI language.
- Sounds: Independent toggles for new event sounds, new inReach message sounds, and radio red-state transition sounds.
- Experimental Features: Toggles for in-development feature flags (only shown when flags are present in the store or query string).
- Map
- Alerts
The hamburger menu opens a left-side drawer. Items are visibility-gated by system config flags and user permissions:
- Alerts: modal iframe of the tenant's alerts page.
- Contact Support: uses the embedded JIRA help widget when available.
- Help Center, Community, Users Guide: external links opening in a new tab.
- Exports: each opens a modal; all are permission- or flag-gated:
- Daily Report
- Master KML
- Subject Information and Subject Reports
- Field Reports
- Ecoscope: sub-menu with links to Ecoscope Downloader and Ecoscope Analysis.
- Footer: server version, client build version, copyright, and EULA / privacy policy links.
- Runtime: Node.js 24
- Package manager: Yarn 4
- UI: React 19,
react-dom; component library viareact-bootstrap(Bootstrap 5) - Routing: React Router 7 (client-only SPA with
BrowserRouter) - Build: Vite 8
- State: Redux 5 +
redux-thunk,redux-promise; persistence viaredux-persist - Map: Mapbox GL
- HTTP: Axios
- Real-time: Socket.IO
- Auth: Auth0 + DAS token exchange
- Geo / utilities:
@turf/turf,proj4,geodesy,date-fns,lodash-es,@dnd-kit - Analytics:
react-ga4; cookie consent via Osano - i18n:
i18next(chained localStorage + HTTP backend) - Lint / style: ESLint, Stylelint
- PWA: Workbox service worker
- Deployment: Docker and GitHub Actions
src/index.js: bootstrapsrc/App.js: authenticated shellsrc/config.js: runtime auth configsrc/store.js: Redux store:thunk+promiseMiddleware+ Redux DevTools extension hooksrc/{ComponentName}/: mostly flat component folders; component-specific styles in co-locatedstyles.module.scsssrc/common/: shared assets and global SCSS partialssrc/ducks/: Redux logic per domainsrc/reducers/index.js:combineReducersroot; wirespersistReducerper slicesrc/selectors/: reselect selectorssrc/hooks/: shared hookssrc/withSocketConnection/: Socket.IO context provider and real-time event bindingsrc/constants/index.js: shared constants and all Vite env exportssrc/utils/: general utilitiessrc/__test-helpers/: fixtures and mocksjest-config/: Jest transformers and asset mockspublic/locales/{locale}/{namespace}.json: translation files
- Prefix:
REACT_APP_ROUTE_PREFIX - Top-level routes:
login,eula,*→ mainApp - In-app navigation: Path segments drive UI state via
getCurrentTabFromURL/getCurrentIdFromURLinutils/navigation.js. Patterns/:tab/*and/:tab/:id/*.- Sidebar tabs:
events,layers,patrols,settings
- Sidebar tabs:
- Prefer function components with
useDispatch/useSelector - All Redux logic lives in single files under
src/ducks/ - State structure:
state.data- API data,state.view- UI state - Add derived state in
selectors/with reselect
Central map instance shared via MapContext.
API_URL and API_V2_URL are assembled from DAS_HOST + path env vars in src/constants/index.js and imported directly in duck files. RequestConfigManager installs Axios interceptors that attach the auth Bearer token, add a USER-PROFILE header when a profile is active, attach a cancellation token, and redirect to login on 401.
Component-specific styles use co-located styles.module.scss. Global partials in src/common/styles/ are imported where needed. SVGs are imported as React components via { ReactComponent as XIcon } from '*.svg'.
Translation files live under public/locales/{locale}/. Each locale directory contains one JSON file per UI namespace. All new user-facing strings must be added to every locale file, properly translated into each locale's language.
The server stores a canonical JSON Schema per event type. Client-side editing and rendering use a flat formElements structure derived from it. Transformation utilities live in utils/v2-event-schemas/.
yarn start: Vite dev serveryarn build: production bundle tobuild/, then service worker (build-sw)yarn build-sw: Workbox / SW generation onlyyarn test: Jestyarn test-ci: CI-oriented Jest flagsyarn lint: ESLintyarn stylelint: SCSS modules
- Functional components with hooks; props destructured in function signature.
- No PropTypes.
- Do not add new comments unless they are genuinely necessary. Assume the reader knows the language and the codebase; well-named functions and variables should carry the meaning instead.
- A comment earns its place only when the code cannot: a non-obvious why (a workaround, a subtle constraint, an intentional deviation), a caveat, or a link to an external reference. Comments that restate what the code does are noise — delete them from your own output before finishing.
- Never narrate the change or your reasoning in a comment (e.g.
// added this,// now using X instead of Y,// this fixes the bug). Comments describe the code as it stands, not how it got there — the diff and commit message are for that. - The same rules apply to test files: describe intent through
describe/itnames, not inline commentary. - Leave existing comments alone unless they are wrong or you are changing the code they document.
- WCAG 2.1 AA compliance.
- Prefer semantic HTML over custom abstractions.
- Ensure full keyboard operability and visible focus states.
- Use ARIA only when native semantics are insufficient.
- All interactive elements must be screen reader compatible and expose correct roles, names, and states.
Test Runner: Jest with jest-fixed-jsdom
Component Testing: React Testing Library with @testing-library/jest-dom
API Mocking: MSW (Mock Service Worker)
Real-time Mocking: socket.io-mock
package.json: Jest configuration.src/setupTests.js: global mocks, polyfills, env vars.src/test-utils.jsx: customrenderthat wraps app providers (i18n, memory router, navigation).src/i18nForTests.js: i18n config, all namespaces loaded.src/__test-helpers/: map mock, store mock, fixtures.
- Co-located
*.test.*files. - Use a local
renderComponenthelper; prefergetByRolequeries.