19 KiB
Advanced Reports — API Design Reference
Version: 1.0
Date: July 9, 2026
Status: Draft — contract for Phase 1 implementation (endpoint not yet implemented)
Scope: Backend API contract for the Advanced Application Report. This document is the single source of truth for both backend and frontend/client development.
Related Documents: ADVANCED_REPORTS_FEASIBILITY.md, ADVANCED_REPORTS_FUNCTIONAL.md, ADVANCED_REPORTS_NON_FUNCTIONAL.md, ADVANCED_REPORTS_IMPLEMENTATION_PLAN.md, ADVANCED_REPORTS_PROPOSAL.md
Table of Contents
- 1 Overview
- 2 Authentication
- 3 Report Generation Flow
- 4 Endpoints
- 5 Generated Artifacts
- 6 Datasource Contract (
rptDS.json) - 7 Error Responses
- 8 Data Model Notes
- 9 Frontend Integration Guide
- 10 Backend Architecture Notes
- 11 Open Decisions
- 12 Changelog
1 Overview
The Advanced Application Report is a mission-level, multi-page report (Mission Overview, Mission Coverage, Zone Detail per zone) generated for a single completed job ("mission"). The server computes all analytics, renders map images, and writes a JSON datasource; the client Stimulsoft viewer renders and exports the report — identical to the legacy report contract.
Base path: /api/jobs
To be implemented in:
controllers/advanced_report.js(new)helpers/report_util.js(new — analytics engine)routes/job.js(new route)public/sprayMap.html(map page variants)reports/app_advanced.mrt(authored manually in the embedded Stimulsoft designer)
The legacy endpoints (/preAppReport, /preLoadReport) are unchanged.
2 Authentication
Same as the legacy report endpoints. All routes require a valid JWT bearer token; the
checkUser middleware is applied globally in server.js.
Authorization: Bearer <jwt>
The /api/jobs route group applies the subscription middleware (checkRqPkgSubscription),
so the caller must hold an active package. The job must belong to the caller's customer
scope; otherwise 401 not_authorized.
3 Report Generation Flow
Client (Report Settings dialog)
│ POST /api/jobs/preAdvancedReport { jobId, rptOp, reportContents, ... }
▼
Server
1. Load job + populated refs (client, operator, vehicle, products, crop)
2. Persist report settings onto the job (rptOp incl. reportContents)
3. Stream ApplicationDetail (by the job's fileIds, projected fields)
4. Analytics engine: per-line → per-zone → mission aggregates (one pass over the data)
5. Render map images (one Chromium instance: mission map, zone maps, thumbnails)
6. Write REPORT_DIR/dat/<genFolder>/rptDS.json + map images
7. Select template: app_advanced_<applicatorId>.mrt else app_advanced.mrt
│ 200 { rid, path, c }
▼
Client (Stimulsoft viewer)
GET /reports/<rid>.mrt
GET /reports/dat/<path>/rptDS.json (+ map images referenced within)
→ render, print, export PDF (client-side)
Generation is synchronous within the HTTP request. Budget: ~35 s per 10 zones, ~15 s for a typical 3-zone job (NFR-1.1). Repeat exports from an open viewer are client-side and cost nothing.
4 Endpoints
4.1 Generate Advanced Report
POST /api/jobs/preAdvancedReport
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
number | yes | The job/mission to report on (Job._id — numeric auto-increment id) |
lang |
string | no | Report language: en (default), pt, es |
rptOp |
object | no | Report settings (persisted onto the job, legacy shape) |
rptOp.printArea |
boolean | no | Print the planned area size |
rptOp.areaSize |
number | no | Planned area (job units; acres converted to ha server-side when measureUnit is US) |
rptOp.coverage |
number | no | Sprayed area (job units) |
rptOp.appRate |
number | no | Application rate override |
rptOp.actualVol |
number | no | Actual spray volume |
rptOp.useActualVol |
boolean | no | Use actualVol instead of computed volume |
reportContents |
object | no | New — Report Contents selections (persisted with rptOp) |
reportContents.includeZoneDetail |
boolean | no | Include Zone Detail pages. Default true |
reportContents.sprayedZonesOnly |
boolean | no | Zone Detail pages only for zones with spray data. Default false; ignored when includeZoneDetail is false |
reportContents.includeFlightLineStats |
boolean | no | Include the flight-line table on Zone Detail pages. Default true |
reportContents.hideMapBackground |
boolean | no | Render all report maps on a plain dark-green background (mockup styling) instead of satellite imagery — smaller files, faster capture. Default false |
useCustWI |
boolean | no | Use manually entered weather instead of logged averages |
weatherInfo |
object | no | Manual weather: { windSpd, windDir, temp, humid } |
Example Request Body
{
"jobId": 10234,
"lang": "en",
"rptOp": { "printArea": true, "areaSize": 6681.1, "coverage": 6201.3, "appRate": 10.0, "useActualVol": false },
"reportContents": { "includeZoneDetail": true, "sprayedZonesOnly": false, "includeFlightLineStats": true, "hideMapBackground": false },
"useCustWI": false
}
Response 200 OK
{
"rid": "app_advanced",
"path": "appadv_10234_1720537200000",
"c": 0
}
| Field | Type | Description |
|---|---|---|
rid |
string | Template id — app_advanced (default) or app_advanced_<applicatorId> (customer-customized) |
path |
string | Generated-artifact folder under REPORT_DIR/dat/ |
c |
number | 1 when a customer-customized template was selected, else 0 |
This is the exact { rid, path, c } contract of the legacy /preAppReport, so the
existing viewer flow needs no changes beyond calling the new endpoint.
4.2 Report Options (existing, reused)
POST /api/jobs/reportOps { jobId }
Unchanged. Returns coverage / actual volume / area size defaults for pre-filling the
Report Settings dialog (values in ha; client converts per measureUnit).
4.3 Save Report Template (existing, reused)
POST /api/jobs/saveReport
Unchanged. The in-product report designer saves an edited template as <rid>.mrt;
saving under app_advanced_<applicatorId> creates the per-customer override (FR-7.1).
5 Generated Artifacts
Written to REPORT_DIR/dat/<path>/ and served from the same static /reports path as
legacy report artifacts (hosting sits outside this Express app; non-guessable folder
names are the effective access control, as with legacy reports — see NFR-4.3):
| Artifact | Description |
|---|---|
rptDS.json |
Full report datasource (section 6) |
map.jpg |
Mission overview map (single-viewport or locator mode, FR-2.3) |
zone_<n>.jpg |
Zone Detail map, one per included zone |
thumb_<n>.jpg |
Coverage-grid thumbnail; absent when zone count > 12 (compact layout, FR-3.5) or cropped from map.jpg in single-viewport missions |
Folder names are server-generated and non-guessable; artifacts are retained and later removed by the separate maintainer app's periodic cleanup (legacy pattern). A repeat request regenerates into a fresh folder.
6 Datasource Contract (rptDS.json)
All display values are pre-localized, pre-formatted strings (units, locale numbers,
local times) — the template renders them exactly as written, with no further processing. Missing/unavailable values are the
em-dash string "–". Optional sections are suppressed via empty datasets, never
empty objects.
{
"reports": { "type": 2 },
"mission": [{
"jobId": 10234,
"name": "Spring Fertilizer 2026",
"jobType": "Fertilizer Application",
"crop": "Corn",
"planDates": "May 22, 2026 - May 22, 2026",
"actualDates": "May 22, 2026, 10:15 AM - 3:57 PM",
"duration": "5h 42m",
"customer": "Greenfield Farms",
"customerAddress": "12 Harvie Road, Barrie, ON",
"pilot": "John Smith",
"licence": "AG-48213-ON",
"aircraft": "Air Tractor AT-802",
"flightNumber": "C-GNAV",
"applicator": "AgMission Aerial Services",
"applicatorAddress": "45 Airport Road, Barrie, ON",
"mapfile": "https://<host>/reports/dat/<path>/map.jpg",
"coveragePct": "95.7%",
"avgSpeed": "143.6 mph",
"avgHeight": "12.3 ft",
"avgXtError": "2.07 ft",
"totalVolume": "12,845 gal",
"zonesSprayed": "5 / 9",
"plannedArea": "6,681.1 ac",
"sprayedArea": "6,201.3 ac",
"totalFlightTime": "5h 42m",
"totalSprayTime": "4h 31m",
"ferryTime": "1h 11m",
"totalDistance": "1,245.2 mi",
"sprayDistance": "903.4 mi",
"ferryDistance": "341.8 mi",
"avgAppRate": "0.50 gal/ac",
"avgFlowRate": "46.8 GPM",
"swathWidth": "60.0 ft",
"remark": "Light crosswind after 14:00; zones 6, 8 and 9 deferred.",
"createdDate": "Jul 9, 2026"
}],
"coverageCards": [{
"zoneNum": 1, "name": "North 40",
"sprayedPlanned": "299.3 / 312.4 ac", "coveragePct": "95.8%",
"thumbFile": "https://<host>/reports/dat/<path>/thumb_1.jpg"
}],
"zones": [{
"zoneNum": 1, "name": "North 40", "crop": "Corn",
"plannedArea": "312.4 ac", "sprayedArea": "299.3 ac", "coveragePct": "95.8%",
"volumeApplied": "625 gal", "avgAppRate": "0.50 gal/ac",
"flightTime": "26m", "sprayTime": "23m", "avgTurnTime": "17.4 s",
"avgSpeed": "145.1 mph", "avgHeight": "12.2 ft",
"avgFlowRate": "46.5 GPM", "avgXtError": "1.90 ft",
"mapfile": "https://<host>/reports/dat/<path>/zone_1.jpg",
"zoneIndexLabel": "Zone 1 of 9"
}],
"lines": [{
"zoneNum": 1, "lineNum": 1, "startTime": "09:15:00",
"sprayTime": "97.2 s", "sprayLength": "4,085 ft", "avgSpeed": "146.1 mph",
"areaCovered": "22.85 ac", "appRate": "0.50 gal/ac",
"avgXtError": "1.80 ft", "turnTime": "17.1 s"
}],
"products": [{
"name": "28-0-0 UAN Blend", "restricted": "No", "epaReg": "–",
"rateStr": "0.50 gal/ac", "totalRateStr": "12,845 gal", "count": 1
}],
"weather": [{
"windSpd": "8.6 mph", "windDir": "215° SW", "temp": "21.8°C", "humid": "56%"
}]
}
Field Notes
reports.type—0planning,1legacy application report,2advanced report.zones[]is already filtered perreportContents(excluded zones don't appear);coverageCards[]always contains all zones regardless of filtering.- Unsprayed zones in
zones[]carry"–"values, a boundary/ferry-onlymapfile, and exactly onelines[]placeholder row of"–"cells (FR-4.6). lines[]is empty whenincludeFlightLineStatsisfalse(template band collapses).- When zone count > 12,
coverageCards[].thumbFileis""and the template renders the compact text layout (FR-3.5). mission.remark—job.remarkverbatim;"–"when the job has none (Remark line, FR-2.10).- Temperature is always °C; every other quantity follows the job's
measureUnit(FR-6.1). - Mission totals are computed in the same data pass as the zone values — they always reconcile (NFR-3.3).
7 Error Responses
Standard AgMission error format:
{ "error": { ".tag": "error_constant_value", "message": "Detail (development mode only)" } }
| HTTP Status | .tag value |
When it occurs |
|---|---|---|
401 |
not_authorized |
Missing/invalid JWT, or job not in caller's scope |
409 |
job_not_found |
Job does not exist |
409 |
invalid_param |
Malformed jobId, unknown lang, invalid option values |
409 |
report_limits_exceeded (new) |
Mission exceeds the supported limits: > 50 zones or > 2,000 flight lines (NFR-2.1) |
429 |
report_busy (new) |
Max concurrent generations (2 per process) reached — client should retry (NFR-2.2) |
500 |
report_generation_failed (new) |
Mission map capture failed or datasource write failed (zone-map failures degrade to placeholders instead, NFR-3.1) |
8 Data Model Notes
- Mission = Job.
Job._idis a Number (auto-increment; there is no separatejobIdfield on Job); zones are thejob.sprayAreaspolygon array. - Report settings persistence:
rptOp(extended withreportContents) is saved onto the job on every request, so the dialog restores the last-used selections per job. Values arrive in job units and are stored metric (acre→ha conversion server-side whenmeasureUnitis US) — same as legacy. - Analytics granularity: per-line and per-zone values are computed on the fly from
ApplicationDetail(read once via a streaming cursor, projected fields, queried by the job'sfileIds — the collection's only index). Nothing new is persisted by report generation. - Known data gaps (render as
"–"):lminAppflat 0 without a flow controller; SatLoc-imported applications lack xTrack/turn statistics; devices without xTrack recording have no XT error anywhere.
9 Frontend Integration Guide
- Open Report Settings; pre-fill from
POST /reportOps(existing behaviour). - Render the Report Contents panel (right side): Include All Zone Detail (default on), nested Sprayed Zones Only (default off, disabled when parent off), Include Flight Line Statistics (default on), each with an info tooltip (FR-7.4).
- On Preview:
POST /preAdvancedReportwith the dialog state; show a progress indicator sized to the NFR-1.1 budget (~15–35+ s; consider zone count). - Hand
{ rid, path }to the existing Stimulsoft viewer component unchanged; the viewer loads the template and datasource and handles print/PDF export client-side. - On
report_busy, offer retry; onreport_limits_exceeded, surface the zone/line limits. - The viewer's
localizeReport()cultures (en-US / pt-PT / es-ES) are guaranteed present inapp_advanced.mrt— no client change needed.
10 Backend Architecture Notes
10.1 Generation Data Flow Diagram
The ApplicationDetail records are read once per report, and the per-line, per-zone
and mission values are all computed during that single pass over the data. No dataset is
produced by a separate query or code path, so the values always agree with each other
(NFR-1.2, NFR-3.3).
flowchart LR
A["Job by jobId"] --> B["Applications and<br/>AppFiles of the job"]
B --> C["fileId list"]
C --> D["Read ApplicationDetail once<br/>streaming cursor,<br/>projected fields"]
D --> E["Line segmentation<br/>by llnum / sprayStat"]
E --> F["Zone assignment:<br/>point-in-polygon<br/>vs job.sprayAreas"]
F --> G["Per-line<br/>stats"]
G --> H["Zone<br/>roll-ups"]
H --> I["Mission<br/>totals"]
G --> J["rptDS<br/>lines dataset"]
H --> K["rptDS zones and<br/>coverageCards datasets"]
I --> L["rptDS<br/>mission dataset"]
10.2 Component Interaction Diagram
flowchart TD
FE["Frontend:<br/>Report Settings dialog"] --> EP["POST /api/jobs/<br/>preAdvancedReport"]
EP --> CTL["controllers/<br/>advanced_report.js"]
CTL --> RU["helpers/report_util.js<br/>analytics engine"]
CTL --> WU["helpers/web_util.js<br/>single shared Chromium"]
WU --> SM["public/sprayMap.html<br/>variants"]
CTL --> FS[("REPORT_DIR/dat/genFolder:<br/>rptDS.json + map images")]
CTL --> TPL{"customer template<br/>app_advanced_applicatorId.mrt<br/>exists?"}
TPL -->|yes| C1["rid = customized<br/>c = 1"]
TPL -->|no| C0["rid = app_advanced<br/>c = 0"]
RU --> J[("jobs")]
RU --> AP[("applications")]
RU --> AF[("application_files")]
RU --> AD[("application_details")]
FE2["Stimulsoft viewer"] --> MRT["GET /reports/<br/>rid.mrt"]
FE2 --> DS["GET /reports/dat/path/<br/>rptDS.json + images"]
10.3 Map Capture Decision Diagram
Capture count follows the effective page selection, not the zone count (NFR-1.3).
flowchart TD
A["Start captures:<br/>one shared browser"] --> B{"Zones fit legibly<br/>in one viewport?"}
B -->|yes| C["Mission map:<br/>full polygons"]
B -->|no| D["Mission map:<br/>locator badges<br/>(FR-2.3.2)"]
C --> E{"More than<br/>12 zones?"}
D --> E
E -->|yes| F["Skip thumbnails:<br/>compact layout<br/>(FR-3.5)"]
E -->|no| G{"Single<br/>viewport?"}
G -->|yes| H["Crop thumbnails from<br/>the mission capture"]
G -->|no| I["Per-zone<br/>thumbnail captures"]
F --> K{"includeZoneDetail?"}
H --> K
I --> K
K -->|yes| L["Zone map capture per<br/>included zone<br/>(sprayedZonesOnly filter)"]
K -->|no| M["No zone<br/>captures"]
10.4 Report Generation Sequence
sequenceDiagram
participant FE as Frontend
participant API as Jobs API
participant DB as MongoDB
participant CH as Shared Chromium
participant FS as REPORT_DIR
FE->>API: POST preAdvancedReport<br/>jobId, rptOp, reportContents
API->>DB: Load job and its related records,<br/>persist report settings
API->>DB: Read ApplicationDetail once<br/>by fileIds, streaming cursor
DB-->>API: Points aggregated to<br/>line, zone, mission values
API->>CH: Render mission map,<br/>thumbnails, zone maps (10.3)
CH-->>API: JPEG captures<br/>(zone-map failure = placeholder)
API->>FS: Write rptDS.json + images<br/>to dat/genFolder
API-->>FE: 200 rid, path, c
FE->>FS: GET template .mrt,<br/>rptDS.json + images
Note over FE: Viewer renders.<br/>Print and PDF export client-side
Concurrency: a simple in-process counter caps generation at 2 concurrent requests
(429 report_busy beyond that, NFR-2.2). The generation function is isolated from the
HTTP layer so it can later move behind the existing worker framework unchanged (NFR-2.3).
11 Open Decisions
| # | Decision | Status |
|---|---|---|
| 1 | Page orientation (portrait-only vs landscape variant) — affects template only, not this API | Awaiting PO (F-OQ-1) |
| 2 | Regeneration reuse/caching for unchanged repeat requests (same {rid, path} returned) |
Deferred — out of Phase 1 scope; API shape already compatible |
| 3 | Exact .tag strings for the new error constants (helpers/constants.js naming review) |
To be finalized during D2 implementation |
| 4 | Compact coverage layout threshold — exact rule (more than 12 vs 12 and above) and threshold value; affects when coverageCards[].thumbFile is empty |
Awaiting PO (F-OQ-2) |
12 Changelog
| Version | Date | Notes |
|---|---|---|
| 1.0 | 2026-07-09 | Initial draft — contract derived from the approved Phase 1 planning set (feasibility, FR, NFR, implementation plan) |
| 1.1 | 2026-07-13 | mission.remark added to mission[] (Remark line on page 1, FR-2.10). Overview-map zone/field names (FR-2.3 rev.) — map rendering only, no contract impact |