agmission/server/docs/ADVANCED_REPORTS_API.md

564 lines
65 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Advanced Reports — API Design Reference
**Version:** 1.4
**Date:** July 27, 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](#1-overview)
- [2 Authentication](#2-authentication)
- [3 Report Generation Flow](#3-report-generation-flow)
- [4 Endpoints](#4-endpoints)
- [4.1 Generate Advanced Report](#41-generate-advanced-report)
- [4.2 Report Options (existing, reused)](#42-report-options-existing-reused)
- [4.3 Save Report Template (existing, reused)](#43-save-report-template-existing-reused)
- [5 Generated Artifacts](#5-generated-artifacts)
- [6 Datasource Contract (`rptDS.json`)](#6-datasource-contract-rptdsjson)
- [7 Error Responses](#7-error-responses)
- [8 Data Model Notes](#8-data-model-notes)
- [9 Frontend Integration Guide](#9-frontend-integration-guide)
- [10 Backend Architecture Notes](#10-backend-architecture-notes)
- [10.1 Generation Data Flow Diagram](#101-generation-data-flow-diagram)
- [10.2 Component Interaction Diagram](#102-component-interaction-diagram)
- [10.3 Map Capture Decision Diagram](#103-map-capture-decision-diagram)
- [10.4 Report Generation Sequence](#104-report-generation-sequence)
- [11 Open Decisions](#11-open-decisions)
- [12 Changelog](#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)
2b. Cache check (added post-Phase-1, changelog 1.11) — hash everything that would
actually change the output; on a match against Job.advRptCache (and only if
the previous rptDS.json is still on disk) skip straight to step 7 with that
prior generation's { rid, path, c }
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. A repeat *generation* call for the same job — not
just a repeat export — also costs nothing beyond the cache check above when
nothing that feeds the report has changed since the last generation (changelog
1.11); a genuine change (new data imported, settings changed, a different
viewer role, etc.) always triggers a full regeneration as before.
---
## 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`. Always follows this request/the saved preference — **not** affected by the requester's role (see note below, which is a separate, narrower restriction on the map imagery only) |
| `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 }` |
**Flight path *map imagery* visibility mirrors the Job Map exactly** (`flightPathViewRoles`,
`helpers/constants.js` — the same set the client's `AuthService.isPlanner` uses to decide
whether the Flight Paths overlay even appears on the Job Map): only `APP` (Applicator/Master),
`APP_ADM` (Office Admin), and `OFFICER` see the ferry/flight-path polylines drawn on the
report's map captures. Every other role — Pilot, Client, Inspector, Admin, Device, Vendor,
Partner — gets maps with spray lines only, flight paths omitted. This is scoped to the map
imagery alone; it does **not** affect the Flight Line Statistics table, which always follows
`reportContents.includeFlightLineStats` regardless of role, consistent with every other
Report Contents toggle. The requesting user's role comes from the auth token (`req.ut`), not
from anything in the request body.
#### Example Request Body
```json
{
"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`
```json
{
"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 (`zones[].mapfile`, section 6) |
| `zone_thumb_<n>.jpg` | Mission Coverage thumbnail for that zone (`coverageCards[].thumbFile`, section 6) — same `focusZone` fit/refit as `zone_<n>.jpg`, captured separately with a heavier polygon boundary stroke so the line prints at the same visual thickness as the Mission Overview/Zone Detail pages despite the thumbnail card's much smaller embed size (~57×36mm vs ~190×135mm) |
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
empty string `""` (rendered as blank space — changed from an em-dash `""` placeholder in
1.5). Optional sections are suppressed via **empty datasets**, never empty objects.
```json
{
"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",
"appRate": "0.50 gal/ac",
"appTotalVolume": "3,340.6 gal",
"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>/zone_thumb_1.jpg"
}],
"zones": [{
"zoneNum": 1, "name": "North 40", "crop": "Corn", "product": "28-0-0 UAN Blend",
"plannedArea": "312.4 ac", "sprayedArea": "299.3 ac", "coveragePct": "95.8%",
"volumeApplied": "625 gal", "avgAppRate": "0.50 gal/ac",
"startTime": "09:15:00", "endTime": "09:41:12",
"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", "type": "Active", "restricted": "No", "epaReg": "",
"rateStr": "0.50 gal", "totalRateStr": "12,845 gal"
}],
"weather": [{
"windSpd": "8.6 mph", "windDir": "215° SW", "temp": "21.8°C", "humid": "56%",
"dataFile": "20260522_143.zip, 20260522_144.zip"
}]
}
```
#### Field Notes
- `reports.type``0` planning, `1` legacy application report, **`2` advanced report**.
- `zones[]` is already filtered per `reportContents` (excluded zones don't appear);
`coverageCards[]` always contains **all** zones regardless of filtering.
- Unsprayed zones in `zones[]` carry `""` values, a boundary/ferry-only `mapfile`, and
exactly one `lines[]` placeholder row of `""` cells (FR-4.6).
- `lines[]` is empty when `includeFlightLineStats` is `false` (template band collapses) —
purely the caller's own Report Contents choice; the flight-path role check above never
affects this.
- When zone count > 12, `coverageCards[].thumbFile` is `""` and the client-side viewer
(`report.component.ts`, not the template) switches the coverage grid to a compact,
map-free text layout (FR-3.5) — part of a three-tier grid density that also gives ≤6
zones a larger 2-column layout and 7-12 zones the original 3-column size; see §12
changelog 1.8.
- `mission.remark``job.remark` verbatim; `""` when the job has none (Remark line, FR-2.10).
- `zones[].product` (added 1.9) — comma-joined names of the job's active-ingredient products
(excludes carriers, same Active/Carrier distinction as `products[].type`); mission-wide,
so every zone in the same job repeats the identical value — same convention already used
by `zones[].crop` (`mission.crop`, not a per-zone crop). Zone Detail's Zone Info panel
shows it directly after Crop.
- `zones[].startTime` / `zones[].endTime` (added 1.9) — the zone's own first and last
spray-on timestamp (`analytics.zones[idx].startTimeS`/`endTimeS`, formerly computed
internally as `_firstT`/`_lastT` and discarded after deriving `flightTime`; now also
exposed directly), formatted `HH:MM:SS` via `todStr()`. `""` for an unsprayed zone, same
as every other zone-level measured field. Template-only placement: Zone Detail's Flight
Statistics box, first two rows on the left column.
- `weather.temp` follows the job's `measureUnit` like every other quantity (°F for US-unit
jobs, °C otherwise, via `utils.inCorF`) — changed in 1.5 from an earlier "always °C"
behaviour that deliberately ignored `measureUnit` for this one field; it's now consistent
with legacy's own weather temperature formatting and every other unit-aware field in this
report.
- `weather[]` fields degrade **independently** as of 1.5 (`helpers/job_util.js
getDataWeatherInfoPerField`) — one implausible sensor field (e.g. a stuck/bad temperature
reading) no longer blanks the other three; each of `windSpd`/`windDir`/`temp`/`humid` is
validated and dashed on its own. The whole `weather[]` array is still empty (suppressing
the section) only when *every* field is unavailable. `weather.dataFile` — the imported
flight file name(s) for the job, comma-joined (same source/join as legacy's
`Application.dataFile`); independent of whether the other weather values are a manual
override or aggregated from real data.
- `mission.appRate` / `mission.appTotalVolume` (added 1.5) — the job's own configured/
overridden Application Rate and the resulting `rate × coverage` volume, formula-for-formula
matching legacy's Application-row `Rate`/`Total Volume Used` (`controllers/job.js`). This is
distinct from `mission.avgAppRate`, which is derived from real flow-sensor telemetry — the
two can legitimately differ (planned vs. measured) and are shown side by side in Mission
Statistics, template-labeled "AppRate" (`mission.appRate`) and "Avg AppRate"
(`mission.avgAppRate`) so a reader isn't left guessing which figure is the pre-flight plan
and which is the real measurement.
- Mission totals are computed in the same data pass as the zone values — they always
reconcile (NFR-3.3).
- `products[].type``"Active"` or `"Carrier"`, derived from `Product.type`
(`APTypes.ACTIVE` / `APTypes.CARRIER`, `helpers/constants.js`). Distinguishes active
ingredients (herbicides, fertilizers, etc.) from carriers (e.g. water) within the single
products table — mirrors the Job Products panel's own "Type" column.
- `mission.swathWidth` — the mission's actual **average recorded swath** (a weighted mean
of real per-point swath readings across every sprayed zone, `helpers/report_util.js`),
not the static `job.swathWidth` configuration value. Falls back to the job's configured
swath only for individual GPS records that carry no recorded swath at all. Template label:
"Swath Width".
- `mission.totalVolume` — defaults to `Σ Application.totalSprayMat` (area swept × recorded
rate, computed at file-import time) — the same source and method as the legacy "Actual
Spray Volume" figure and the job-map-edit playback's "Mat Sprayed" total, kept consistent
across the app. This replaced an earlier flow-rate/time integration unique to this report
(`report_util.js`'s `mission.volumeL`, still used only for `avgFlowRate`). Manual override
via `rptOp.useActualVol`/`rptOp.actualVol` still takes precedence when set. Template label
is "ACTUAL VOLUME" (JSON key unchanged for compatibility).
- `mission.avgXtError` — a **flat, unweighted average** across every individual spray-on
cross-track reading in the mission, deliberately bypassing the zone-weighted-by-spray-time
mean used by the other `avg*` fields (`avgSpeed`, `avgHeight`, etc.). This intentionally
matches the client playback's own average-XT calculation method (`job-map-edit.component.ts`
`playXt`) so the two figures are computed the same way; they can still differ in value if
playback reflects a smaller scrubbed/loaded subset of the mission's data than the full
server-side recompute.
---
## 7 Error Responses
Standard AgMission error format:
```json
{ "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._id` is a Number (auto-increment; there is no separate `jobId` field on Job); zones are the `job.sprayAreas` polygon array.
- **Report settings persistence**: `rptOp` (extended with `reportContents`) 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 when
`measureUnit` is 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's
`fileId`s — the collection's only index). Nothing new is persisted by report generation.
- **Known data gaps** (render as `""`): `lminApp` flat 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
1. Open Report Settings; pre-fill from `POST /reportOps` (existing behaviour).
2. 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).
3. On **Preview**: `POST /preAdvancedReport` with the dialog state; show a progress
indicator sized to the NFR-1.1 budget (~1535+ s; consider zone count).
4. Hand `{ rid, path }` to the existing Stimulsoft viewer component unchanged; the viewer
loads the template and datasource and handles print/PDF export client-side.
5. On `report_busy`, offer retry; on `report_limits_exceeded`, surface the zone/line limits.
6. The viewer's `localizeReport()` cultures (en-US / pt-PT / es-ES) are guaranteed present
in `app_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).
```mermaid
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
```mermaid
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).
```mermaid
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["thumbnails off:<br/>compact layout<br/>(FR-3.5)"]
E -->|no| G["thumbnails on —<br/>every zone needs its own capture"]
F --> K{"includeZoneDetail<br/>for this zone?"}
G --> L["Zone map capture<br/>(focusZone, sprayedZonesOnly filter) —<br/>serves Zone Detail AND the<br/>coverage-card thumbnail, no separate crop"]
K -->|yes| L
K -->|no| M["No zone<br/>capture"]
```
### 10.4 Report Generation Sequence
```mermaid
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/>zone maps — also serve as<br/>coverage thumbnails (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) | **Implemented** post-Phase-1 (changelog 1.11) — hash-based check before the analytics/capture work; API shape and response unchanged |
| 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 |
| 1.2 | 2026-07-17 | `products[].type` added (`"Active"` / `"Carrier"`, from `Product.type`) so the template can distinguish carriers from active ingredients within the single products table. `products[].count` removed — it held the job-wide product count repeated identically on every row (no per-row meaning) and was dropped rather than kept as dead data. Template-only: products table "Rate" column relabeled "Rate/Ac"; weather section consolidated from a 2-row header+data table into a single bordered row (label + value per metric) — no datasource shape change. |
| 1.3 | 2026-07-23 | `mission.swathWidth` now computed as the mission's actual average recorded swath (`report_util.js`), replacing a verbatim echo of the static `job.swathWidth` setting — the two could previously disagree with what the equipment actually recorded. `mission.totalVolume`'s default source changed from a flow-rate/time integration to `Σ Application.totalSprayMat`, matching the legacy "Actual Spray Volume" / playback "Mat Sprayed" method for consistency; JSON key unchanged, template label relabeled "ACTUAL VOLUME" (was "TOTAL VOLUME"). `mission.avgXtError` changed from a zone-weighted-by-spray-time mean to a flat, unweighted average across every spray-on reading, matching the client playback's own XT calculation method. Template-only: KPI labels "AVG SPEED"/"AVG HEIGHT" relabeled "AVG SPR SPEED"/"AVG SPR HEIGHT" (en/pt/es) — no datasource shape change from this or the totalVolume relabel. |
| 1.4 | 2026-07-27 | `coverageCards[].thumbFile` now always points at `zone_<n>.jpg` (the same independent per-zone `focusZone` capture used for that zone's own Zone Detail map) instead of a separate `thumb_<n>.jpg` crop of the mission-wide capture — the crop's native resolution and framing depended on the shared mission viewport's zoom, which produced inconsistent thumbnails across jobs (blurry/thick boundaries for a small zone next to large ones; crops dominated by ferry-track clutter when a widely-separated sibling zone forced a very zoomed-out mission view). `thumb_<n>.jpg` is no longer produced. Zone Detail and coverage-thumbnail map captures now also show only the focused zone's own spray corridors and flight-path segments (both tagged with the zone they belong to), fading/hiding neighbouring zones' data instead of showing everything within the capture's frame — map rendering only, no other datasource shape change. |
| 1.7 | 2026-07-31 | Template-only, both `app_advanced.mrt` and the per-applicator override. No datasource shape change. **Two structural template fixes carried over from 1.5/1.6's regression** (the override file's `app_advanced_<applicatorId>.mrt` had been accidentally overwritten with a much older, pre-1.2 copy and was rebuilt from that baseline back up to parity — see "Template recovery" note below): Zone Detail's header banner (`pnlBanner3`) is now a genuine `StiPageHeaderBand` (`PageHeaderBand3`) instead of a static child nested inside `ZoneBand`, so it correctly repeats when a zone's Flight Line Statistics table overflows onto a continuation page (previously that continuation page had no header at all in the override file's older layout). Discovered and documented a real Stimulsoft rendering behavior in the process: **a band's own `ClientRectangle` Y position is not used to place it — the engine stacks each band immediately after the actual rendered height of whatever band precedes it, and only *static* (non-band) child components honor their own authored relative offset within that space.** This means the vertical gap below any repeating page header is controlled by that header band's own height versus its visible content's height (e.g. `PageHeaderBand3`'s 18mm visible banner needs `Height: 21` for a 3mm gap), never by the following content band's declared `Y`; a table/data band that resumes on a continuation page discards its own and its ancestors' offsets and resumes flush against the header. Applied consistently to the Mission Overview (`ReportTitleBand1`/`MissionBand`), Mission Coverage (`PageHeaderBand2`/`coverageBand`), and Zone Detail (`PageHeaderBand3`/`ZoneBand`) pages so the gap above/below each page's first info block matches page-to-page. Also fixed: `pnlMissionFacts` (Mission Info box) had an unintended visible border (removed, matching the borderless `pnlProducts`/`pnlWeather`/`pnlRemark` convention); the weather table's "Flight Data File(s)" column was missing the border/fill/center-alignment styling the other four columns had, so it rendered as unstyled text floating outside the table grid; the Mission Coverage grid's row-to-row vertical gap silently collapsed to 0 despite a taller declared row height because `coverageBand` had `CanShrink: true` (now `false`), and the grid's card title ("1. South 421") margin was widened from 1mm to 3mm to match the info rows below it; removed the horizontal rule above every page's footer (`PageFooterBand1/2/3`'s `Top` border) per explicit request. **Template recovery note**: earlier this session both `.mrt` files were accidentally truncated to 0 bytes by a scripting mistake; they were restored from the most recent available backup (predating most of 1.21.6) and every subsequent fix in this changelog from 1.2 onward was manually reconstructed and re-verified against this session's history and screenshots rather than restored byte-for-byte — flagging in case any earlier-vintage template behavior surfaces that doesn't match a pre-incident report. |
| 1.6 | 2026-07-30 | `coverageCards[].thumbFile` now points at a new, separately-captured `zone_thumb_<n>.jpg` artifact instead of reusing `zone_<n>.jpg` (see §5) — same independent per-zone `focusZone` fit/refit, but captured with a heavier polygon boundary stroke (`window.setZoneStrokeWeight` in `sprayMapAdvanced.html`) so the line prints at the same visual thickness as the Mission Overview/Zone Detail pages despite the Mission Coverage card's much smaller embed size (~57×36mm vs ~190×135mm — the same fixed-pixel stroke width prints ~3x thinner once squeezed into the smaller box). `zones[].mapfile` is unaffected, still `zone_<n>.jpg`. Template-only (per-applicator override file only — the base template was already correct): fixed a regression where the products data band was again mis-nested under the weather panel instead of the products panel, the weather data band's only child was a stray Remark panel that itself mixed in the real weather-value fields (wind speed/direction/temperature/humidity/flight file), and none of the three sections had the `ShiftMode: IncreasingSize` flag needed for a grown band to push later content down — together these caused the products rows to render with no visible header, the weather values and remark text to overlap in the same cells, and a large blank gap before the page footer. Restructured to match the base template's layout (products header+band together, weather header+band together, remark as its own trailing section, all three with `ShiftMode: IncreasingSize`). Also fixed a per-applicator override defect where the header logo referenced an external `Dictionary.Resources` image with a genuinely transparent background — Stimulsoft's PDF export doesn't reliably composite that transparency, so it printed as white instead of the header's green; the logo image is now flattened onto an opaque copy of the header's green so no renderer-side alpha handling is required (matches how the base template's own logo was already built). No datasource shape change other than the `thumbFile` path. |
| 1.5 | 2026-07-30 | `mission.appRate` / `mission.appTotalVolume` added — the job's configured/overridden Application Rate and its resulting `rate × coverage` volume, formula-for-formula matching legacy's Application-row `Rate`/`Total Volume Used`; distinct from the flow-telemetry-derived `mission.avgAppRate`. `weather[]` fields (`windSpd`/`windDir`/`temp`/`humid`) now degrade independently (`helpers/job_util.js getDataWeatherInfoPerField`) — one implausible sensor field no longer blanks the other three, unlike the shared all-or-nothing filter still used by legacy's own weather query. `weather.temp` changed from an unconditional °C to following the job's `measureUnit` like every other quantity (°F for US jobs), matching legacy's own weather-temperature formatting. `weather.dataFile` added — comma-joined imported flight file name(s), same source as legacy's `Application.dataFile`. Global convention change: missing/unavailable values are now the empty string `""` (blank) instead of the em-dash `""` placeholder used since 1.0. Template-only: Mission Statistics restructured into a 3-column layout grouping all four rate/volume figures together (`AppRate`, `Avg AppRate`, `Avg Flow Rate`, `Total Volume`) for direct before/after comparison; weather section rebuilt from a single label/value row into a proper header+data table (mirroring the products table's own layout) with a 5th "Flight Data File(s)" column; KPI tile values centered and resized; colons added to Mission Info, Mission Statistics, Mission Coverage grid, and Zone Detail Flight Statistics labels for a consistent label style; several labels relabeled (`Job Type`→`Job`, `Total Duration`→`Mission Duration`, KPI `COVERAGE`→`COVERAGE %`, `Avg App. Rate`→`Avg AppRate` on both Mission Statistics and Zone Detail); Mission Overview's top map now hides spray corridors and flight-path lines for every user role (zone polygons + number/name/area labels only) — Mission Coverage thumbnails and Zone Detail maps are unaffected and still show per-zone spray/flight detail. Fixed a per-applicator override template defect where the products data band had been mis-nested under the weather panel instead of the products panel, and three stray divider components had been duplicated into the products table rows. No datasource shape change from the template-only items. |
| 1.8 | 2026-08-10 | Template-only, both `app_advanced.mrt` and the per-applicator override. No datasource shape change. Mission Coverage grid's baked-in default resized from 3 columns to 2 (`Columns`/`ColumnWidth`: `3`/`60` → `2`/`92.5`), every card element scaled up to match (~1.54×), and the map thumbnail height bumped a further +6mm on top — fixes the grid leaving most of the page blank for small zone counts under the old fixed 3-column size. Verified against the worst case (6 zones, 3 rows) via a Puppeteer harness driving the real Stimulsoft engine before landing, so the taller thumbnail couldn't silently push a row onto a new page. Paired with a new **client-side** (`report.component.ts`, not template) mechanism: the coverage grid's `Columns`/`ColumnWidth`/component `left`/`top`/`width`/`height` are mutated on the *loaded* Stimulsoft report object based on `coverageCards.length`, before render — confirmed via the harness that these are plain settable properties post-load and the render reflects the mutation, not just the property read-back. Three tiers replace the old binary "grid ≤12 / compact table >12" split: **≤6 zones** use the `.mrt`'s new 2-column default untouched; **7-12 zones** get JS-reset to the original 3-column/60mm card size; **>12 zones** get a compact map-free text grid (thumbnail height forced to 0, 60mm columns reused from the 7-12 tier rather than re-deriving narrower text-box widths — an initial 44mm-column attempt truncated "Sprayed / Planned:", caught via the harness before shipping). Because this lives in JS rather than the template, it applies uniformly to whichever `.mrt` got loaded without duplicating per-file layout work. **Explored and reverted, not shipped**: splitting the >12 tier's "Sprayed / Planned:" into separate `plannedArea`/`sprayedArea` fields plus new `crop`/`volumeApplied` fields (would have added 8 new template components + 4 new `coverageCards` Dictionary columns per file, plus `advanced_report.js` computing those fields using the same `volumeScale`-adjusted volume as the Zone Detail page). Fully implemented and verified working, then reverted at product's request pending a team-lead decision — partly because it made the >12 tier's per-zone blank-space-below-the-grid problem (the same class of issue this version's 2-column fix addresses for small zone counts) more visible for zone counts on the low end of the >12 range (e.g. 13-14 zones only fill ~5 short rows). Zero net change in any shipped file; noted here so the idea isn't silently rediscovered. |
| 1.10 | 2026-08-14 | Map rendering only, no datasource shape change. Street/road name labels now show on report map captures: premium accounts' Google `hybrid` base layer (`initMapBaseLayer`, `utils.js`) was requesting `type: 'hybrid'` but calling `makeGMapStyle()` with no arguments, which explicitly forces `visibility: 'off'` for both the `labels` and `road` style rules — defeating the point of choosing `hybrid` over plain `satellite`; now called as `makeGMapStyle(true, true)`. Non-premium accounts get a new Esri `ImageryTransportation` overlay layer added alongside the existing `Imagery` satellite layer for the same effect, with `window.loaded` now waiting on both layers' own `load` events via a shared pending-count so this doesn't race the existing tile-settle logic. This lives in the shared `initMapBaseLayer` helper used by `sprayMap.html`/`downloadMap.html` as well, so it also affects the Legacy Application Report, not just Advanced. Advanced-Report-only: field coordinates now shown in degrees/minutes/seconds (DMS) via the existing `L.Control.MapCenterCoord` control (`sprayMapAdvanced.html`), which the page loaded but never actually enabled (`params.coors` now defaults to `'DMS'`). Also fixed a race in the `mission`-variant `window.loaded` settle-timer (`sprayMapAdvanced.html`): setting `window.loaded = true` scheduled a delayed flip to the real loaded flag but never cancelled a previously-scheduled one, so a stale tile-load signal from an earlier resize/refit (e.g. `resizeMapContainer`'s `invalidateSize()` firing before `focusZone`'s `fitBounds()` settles) could win the race and mark the page loaded before the final view had actually finished rendering — visible as partial grey map bands, most reproducible on Zone Detail with "Include Flight Line Statistics" off, where the enlarged square map box needs an extra intermediate tile fetch. Every `window.loaded` set now cancels any pending settle timer before scheduling its own. |
| 1.9 | 2026-08-10 | `zones[].product`, `zones[].startTime`, `zones[].endTime` added (see §6 Field Notes) — `product` is mission-wide (comma-joined active-ingredient names, same value on every zone in a job); `startTime`/`endTime` expose the zone's own first/last spray-on timestamp, previously computed internally (`_firstT`/`_lastT`) and discarded after deriving `flightTime`. Template-only otherwise, both `app_advanced.mrt` and the per-applicator override. Zone Detail's Zone Info panel gained a Product row (placed right after Crop) and lost Avg App Rate (moved into the Flight Statistics box instead, see next). The Flight Statistics box was fully rearranged into two columns — left: Start Time, End Time, Flight Time, Spray Time, Avg Turn Time; right: Avg Speed, Avg Height, Avg XT Error, Avg App Rate, Avg Flow Rate — and its padding equalized to a uniform 2mm on all four sides (was 2mm top/left/right but 4mm bottom). **Fixed a Mission Overview layout bug**: a job with more than the ~1 product/weather row the template was originally sized for (e.g. 4 products) grew the Products/Weather panels via their existing `ShiftMode: IncreasingSize`, which pushed the trailing Remark panel past `MissionBand`'s own fixed declared height — since the band had no `CanGrow`, this forced an unnecessary near-empty continuation page containing only the Remark line, even though the physical page still had unused room below the Weather table (that room belonged to the *page*, not to the band's own capped allotment, which is what `CanBreak` actually checks). Fixed by adding `CanGrow: true` to `MissionBand` and tightening the fixed gaps between Mission Facts/Map/KPI-cards/Mission-Statistics/Products/Weather/Remark from their original ~3-4mm down to a mix of 1.5-3.5mm (final values: Mission Facts→Map 1.5mm, Map→KPI 3mm, KPI→"Mission Statistics" label 3mm, label→stats box 1mm, stats box→Products 3mm, Products→Weather 3mm, Weather→Remark 3.5mm) — verified against the same 4-product reproduction case, page count no longer grows. **Explored and reverted, not shipped**: relocating the Remark panel onto the Mission Coverage page instead of Mission Overview, so any residual overflow would land somewhere with reliable spare room rather than spawn a blank continuation page. Fully implemented (a new `RemarkBand` on Page2 bound to `mission`) and verified working, then reverted per explicit request in favor of the gap-tightening approach above, keeping Remark on Mission Overview. Similarly tightened the Zone Detail page's Zone Info block→Zone Map gap (3mm→3.5mm) and Zone Map→"Flight Line Statistics" label gap (4mm→3mm). Mission Coverage grid's card-to-card spacing (the ≤6-zone tier's own baked-in default, untouched by the 1.8 tiering logic) unified to a consistent 6mm in both directions — `ColumnGaps` 5mm→6mm (horizontal) and `pnlCard` height 75.8mm→76mm against an unchanged 82mm row stride (vertical). All three pages' header banners: logo and title-line margins reduced from 8mm to 6mm on both sides (previously already equal at 8mm, now equal at the smaller value); "Advanced Application Report" title font bumped 10pt→11pt; that title nudged down 1.5mm to visually align with the logo's optical center — its box was already geometrically centered in the 18mm banner, but the phrase has no descenders (no g/y/p/q), so line-height-based centering left it sitting visibly high relative to the logo, confirmed by pixel-measuring both elements' actual rendered ink before and after. Per-applicator-override-only: removed a 1mm left inset the logo image had within its wrapper "chip" panel (a structural difference only present in that file — the base template's logo has no such wrapper). |
| 1.11 | 2026-08-19 | Two backend changes, no datasource shape change. **(1) Regeneration cache** (`controllers/advanced_report.js`, `model/job.js`) — resolves Open Decision #2 above. A repeat `preAdvancedReport` call now hashes everything that would actually change the output (zone/exclusion geometry, `job.rptOp`/`useCustWI`/`weatherInfo`, applicator, an imported-data fingerprint via each `App.updateDate``ApplicationDetail` rows carry no timestamp of their own, so a completed/reprocessed import is what actually invalidates this, not the raw detail rows — Report Contents, `dataOp`, language, and the requester's flight-path-visibility role) against a new `Job.advRptCache` field saved from the prior generation; on a match, and only if that prior run's `rptDS.json` is still present on disk, the whole analytics/capture pipeline is skipped and the previous `{rid, path, c}` is returned directly. Template selection is always recomputed fresh regardless, since a `.mrt` file can be added/removed independently of anything that would invalidate the cache. **(2) Map-capture reliability/performance** (`public/sprayMapAdvanced.html`, `public/js/utils.js`) — replaces 1.10's settle-timer patch (the "cancel any pending settle timer" fix) with an adaptive mechanism that waits for each basemap's own authoritative "finished" signal instead of any fixed or DOM-inferred delay: Google's real `tilesloaded` event for the premium satellite basemap (captured once via GoogleMutant's one-time `spawned` event, then reused for every later refit) and Leaflet's own repeatable `load` event for the plain Esri/OSM layers, falling back to watching for `<img>`-specific DOM activity only when neither basemap reference is available yet. A `window.loaded` gate stops `initMapBaseLayer`'s own premature write (tied to `spawned`, not to when tiles actually render) from winning the race against the real signal. Fixed a related bug, confirmed on a real 5-zone job: refocusing the *same* zone a second time for its Mission Coverage thumbnail (identical camera position to the just-captured Zone Detail shot) never receives a new tile-load event at all, since an unchanged view requests no new tiles — `window.focusZone` now recognizes a same-zone refocus and skips straight to a short fixed settle instead of waiting on a signal that will never arrive. Verified against that job's real data: the batch of 12 captures (map + 5 zone details + 5 thumbnails) went from 602s wall-clock with 10 of 12 shots failing outright (each idling out to the 60s per-shot timeout) down to ~5.4s with all 12 succeeding. Separately, fixed a data-correctness bug in the shared `acreToHa`/`haToAcre` helpers (`helpers/utils.js`) discovered while building the cache above: the two used different acre↔hectare conversion constants (`2.471` vs. `2.47105`), so a value round-tripped through both (as `rptOp.areaSize`/`coverage` are, every time the Report Settings dialog is reopened) drifted by a small but real amount each cycle — corrected to the same constant on both. Shared helper; also affects the legacy report's use of the same functions. |
| 1.12 | 2026-08-19 | Template-only (`GlobalizationStrings` only — no component layout/structure change), both `app_advanced.mrt` and the per-applicator override, in the live `REPORT_DIR` copies. No datasource shape change. Found via a systematic audit (walked every static-text component in the template and cross-referenced it against every registered `GlobalizationStrings` entry, rather than spot-checking) that nine labels had **no localization entry in any culture at all** — they rendered in English regardless of report language because no override existed for the translation engine to substitute: `lbZnProduct`/`lbZnStartTime`/`lbZnEndTime` (Zone Detail, added 1.9 but never hooked up), `lbFsAppRate` ("Avg AppRate:" in the reorganized Flight Statistics box, also from 1.9), `lbLineOrderNote` (the "Sorted by actual flight time..." note above the flight-line table), `lbCreated1`/`lbCreated2`/`lbCreated3` (the footer "Created" label, once per page type), and `lbFlightFiles` (weather table's "Flight Data File(s)" column header — present since 1.5). Two more (`lbAppRate`/`lbAppTotalVol`, "AppRate:"/"Total Volume:" on Mission Statistics, added 1.5) had an en-US entry but no pt-PT/es-ES translation. Added en-US entries for the first group (matching each component's existing baked-in English text, for consistency with every other label in the template) and pt-PT/es-ES translations for all eleven, cross-checked against already-translated adjacent labels (e.g. `lbZnAppRate`'s "Taxa Média:"/"Tasa Media:") for consistent terminology. Both live files backed up before editing and the change verified as a pure JSON addition (zero lines removed) before and after. The version-controlled copies of these two files under this branch's own `reports/` directory are a separate, larger concern — they're already out of sync with the live `REPORT_DIR` copies from before this fix (missing entire components in places, not just translations) — and were intentionally left untouched here rather than folded into this fix. |
| 1.13 | 2026-08-19 | `mission.farm` and `zones[].farm` added — same underlying `job.farm` field the legacy report already shows labeled "Farm:" (`controllers/job.js:1221`); Advanced Report never surfaced it until now. `zones[].farm` is mission-wide (same value repeated on every zone, same pattern as `zones[].product`/`crop`). Template changes, both `app_advanced.mrt` and the per-applicator override, live `REPORT_DIR` copies: added a "Farm:" row to the Mission Overview page's Mission Facts panel (left column, right after "Job:", before "Crop:") and to the Zone Detail page's Zone Info panel (right after "Zone:", before "Crop:") — new `pnlFarm`/`pnlZnFarm` components with `lbFarm`/`lbZnFarm` labels and `txtFarm`/`txtZnFarm` value fields, GlobalizationStrings added for all three cultures ("Farm:"/"Fazenda:"/"Finca:" — the last chosen to match this dataset's own real-world terminology, e.g. zone names like "FINCA 12"/"FINCA 66"). Every component below each new row was shifted down 5mm (one row height) to keep existing spacing exactly intact — computed precisely from each panel's known row-height convention (2mm margin + 5mm/row) rather than by eye, and verified end-to-end against real job data using a purpose-built offline Stimulsoft-rendering harness (loads the real `.mrt` + a live job's `rptDS.json` through the same `StiReport`/`StiViewer` API sequence `report.component.ts` uses, screenshotted via Puppeteer) before touching the live files. Separately, per explicit request: removed the "AppRate:" row from Mission Statistics (mission-level `mission.appRate` — the job's configured/overridden rate, distinct from the flow-derived `mission.avgAppRate` which stays) and rebalanced the remaining rows into three even 4-row columns instead of the previous 5/4/4 split — column 3's `AvgAppRate`/`AvgFlowRate`/`TotalVolume` each shifted up one row into AppRate's vacated slots, "Ferry Time" moved from column 1 into column 2's now-free 4th row, and "Swath Width" moved from column 2 into column 3's now-free 4th row; the column divider lines shortened to match the new uniform row count. `mission.appRate` itself is unchanged in the datasource (still computed, just no longer displayed) in case a future template revision wants it back. |
| 1.14 | 2026-08-19 | Removed the "Mission Duration:" row from the Mission Overview page's Mission Facts panel — redundant with "Total Flight Time:" already shown in Mission Statistics just below. `pnlDuration` (and its `lbDuration`/`txtDuration` label+value pair) deleted from both live `.mrt` files; `pnlMissionFacts` height reduced by 5mm (one row) and every component below it in the page's vertical stack — mission map, KPI cards, "Mission Statistics" heading, `pnlMissionStats` — shifted back up 5mm, undoing the downward push the 1.13 Farm-row insertion required, so everything from Products/Weather/Remark down is unaffected. `mission.duration` is left in place in the datasource (unused by the template now, harmless) rather than removed, matching the precedent set for `mission.appRate` in 1.13. Orphaned `lbDuration.Text` `GlobalizationStrings` entries removed across all three cultures. Verified via the same offline Stimulsoft-rendering harness against the live override file and real job 106 data before and after the edit — clean row removal, no gaps or overlaps. |
| 1.15 | 2026-08-19 | Tightened the excess whitespace left at the bottom of the Mission Statistics box (below "Total Spray Time:"/"Ferry Time:"/"Swath Width:") after 1.13/1.14's net changes left its 4 content rows (20mm) sitting in a 30mm-tall panel. `pnlMissionStats` height reduced 30→25 (content ends at y=22 inside the panel; 25 leaves a consistent, tight 3mm bottom margin matching the panel's own top margin), and Products/Weather/Remark shifted back up 5mm to close the resulting gap. Applied to both live `.mrt` files after backup, verified via the same offline rendering harness against the live override file. |
| 1.16 | 2026-08-19 | Fixed a real production case (job 106, 6 products) where a long product list pushed Mission Overview's "Remark:" row past the page's fixed budget, spilling it alone onto its own near-empty continuation page. Predicting that overflow exactly would mean re-implementing Stimulsoft's text-layout engine client-side, so instead: `mission.remarkOnCoverage` (new field, `controllers/advanced_report.js`) is a simple, deterministic proxy — `true` when `products.length > 5` — computed once at datasource-build time. Both live `.mrt` files gained a `pnlRemark2`/`lbRemark2`/`txtRemark2` mirror of Mission Overview's Remark row, placed on the Mission Coverage page as a standalone component (not nested in `coverageBand`). `report.component.ts` now reads `mission.remarkOnCoverage` after `regData` and toggles `.enabled` on the two mirrored rows accordingly — `pnlRemark` (Mission Overview) when off, `pnlRemark2` (Mission Coverage) when on — and, since a plain `StiPanel` placed directly on a page does not auto-stack after a preceding repeating data band the way two Bands would, computes `pnlRemark2`'s absolute `top` from `coverageBand.top` plus `Math.ceil(zoneCount / columns) * rowHeight`, using the same per-tier column/row-height constants as the existing coverage-grid-density mutation (§6 D4 item 1) immediately above it in the same function. Verified via the offline rendering harness for both the ≤5-product (Remark stays on Overview, unchanged) and >5-product (relocated cleanly below the Zone Thumbnail Grid, no overlap) cases, against the live override `.mrt` and job 106's real datasource. Requires a client rebuild (`report.component.ts` changed) to take effect — not deployable via a live-file-only edit like 1.131.15. |
| 1.17 | 2026-08-20 | Tightened the Mission Coverage grid card's vertical spacing for the ≤6-zone tier (the `.mrt`'s own baked-in default — the 7-12 and >12 tiers already use tight, hand-picked constants mutated in `report.component.ts` and were untouched). Was: a 6.23mm zone-name row followed by two 4.98mm rows (Sprayed/Planned, Coverage %) each separated by a ~1.25mm gap, looser than the 5mm-contiguous-row rhythm used everywhere else (Mission Statistics, Flight Statistics). `txtCardName`/`lbCardSprayed`/`txtCardSprayed`/`lbCardCoverage`/`txtCardCoverage` are now three 5mm rows, back-to-back with zero inter-row gap, starting 1.5mm below the thumbnail. Margins settled, after three rounds of feedback, on explicit product-specified values rather than derived ones: 4mm left/right inset (previously ~4.63mm, inherited from the original template) and 3.75mm bottom margin below Coverage % (previously 1mm, then 4.63mm to match left/right, then 3.5mm, landing at 3.75mm as the final call). `txtCardName` spans the full 4mm-to-4mm content width; the Sprayed/Planned and Coverage % rows keep their original label-width proportion (43.17mm) but the value column now stretches to the new 4mm right margin. `pnlCard` height net 76→73.6mm, `coverageBand`'s declared height (the grid's per-row pitch) net 82→79.6mm by the same amount, preserving the existing gap between rows of cards exactly. `cardThumb` (the map thumbnail itself) was never touched by any of this — the height reduction is entirely from the tightened text rows and margins below them. Applied to both live `.mrt` files after backup, verified via the offline rendering harness against real job 106 data (before/after comparison) and the live override file directly. |
| 1.18 | 2026-08-20 | Fixed a real regression in 1.16's Remark-relocation feature, caught on job 108 (5 zones, ≤6-zone tier, 6 products): `report.component.ts` computed `pnlRemark2`'s position using `coverageBand.height` as the grid's row pitch, which had been correct when 1.16 shipped but went stale the moment 1.17's card-spacing pass retuned that same height (82→79.6mm) without anyone updating this duplicate. Investigating further (via the render harness against job 108's actual data) surfaced a second, independent problem: `coverageBand`'s own declared height is never a reliable row-pitch proxy at all — the band's `CanShrink` behavior collapses it to the card's real rendered content height at render time, so even a freshly-correct copy of that number silently overestimates the true row pitch. For 5 zones (3 rows in the 2-column ≤6-zone tier) this pushed Remark's computed `top` far enough down that it landed either past the page (invisible) or close enough to the edge to spill onto a blank continuation page — reproducing the exact overflow bug 1.16 exists to fix, just for Remark's own relocated copy instead of the original. Fixed by reading `pnlCard.height` instead — the same property the grid-density mutation block already sets explicitly per tier, so it can't drift out of sync with whichever tier is active. Verified via the harness against job 108's real data: Remark now renders in-page, above the footer, matching the expected layout. |
| 1.19 | 2026-08-20 | Changed relocated Remark's placement, per product feedback: rather than sitting directly below the Zone Thumbnail Grid, it now anchors just above the page footer — matching how job 108's 3-row ≤6-zone grid happened to look (grid nearly fills the page there), instead of the large, inconsistent-looking gap a lower-row-count grid otherwise leaves (confirmed on job 96's 9-row >12-zone tier and job 109's 3-row 7-12-zone tier, both real jobs). `report.component.ts` now reads `PageFooterBand2.top` (269.4mm on the per-applicator override, 262mm on the base `.mrt` — read dynamically since it differs between the two) and reserves a fixed 20mm text budget plus a 3mm gap above it (matching the ~3mm gaps used between other sections throughout the report) as the preferred position, falling back to directly-below-the-grid only when the grid itself already extends past that anchor point — a near-full 7-12-zone tier (10-12 zones) is the one case where this can still happen, an inherent page-space constraint no placement choice avoids. Verified against all three real jobs (108, 96, 109) via the render harness. |
| 1.20 | 2026-08-20 | Mission Overview map (`missionMap`) height increased 104→110mm; `pnlKpiCoverage`/etc., `lbMissionStats`, `pnlMissionStats`, `pnlProducts`, `pnlWeather`, and `pnlRemark` all shifted down 6mm to match, `MissionBand`'s declared height grown by the same amount. Applied to both live `.mrt` files after backup. This ate 6mm out of the same page-space margin 1.16's overflow fix depends on — reverified empirically via the render harness (real job 108 data, trimmed to 2/3/4/5/6 products) that the safe cutover moved from 5 products to 4: with the taller map, exactly 5 products now overflows Mission Overview onto a blank continuation page (previously safe up to 5), while 4 still fits. `mission.remarkOnCoverage`'s threshold (`controllers/advanced_report.js`) tightened from `products.length > 5` to `> 4` to match, and reverified fixing the 5-product case (relocates cleanly to Mission Coverage, no overflow) without disturbing the real 6-product job108 case already covered by 1.16/1.18/1.19. |
| 1.21 | 2026-08-20 | `missionMap` height reduced 110→107mm (net +3mm vs. the pre-1.20 original 104mm), everything below it on the page shifted up 3mm to match. This recovers 3mm of the margin 1.20 spent, but the `remarkOnCoverage` threshold was left at `>4` products rather than loosened back — being conservative here costs nothing (relocating one product-count earlier than strictly required isn't a visible defect, unlike the under-relocation that caused 1.20's regression), so no threshold change accompanies this entry. Reverified via the render harness that the 4-product boundary case still fits Mission Overview without overflow (now with extra margin to spare) and the real 6-product job 108 case is unaffected. Applied to both live `.mrt` files after backup. |
| 1.22 | 2026-08-20 | `missionMap` height reduced 107→106mm; the gap between the map and the KPI tiles increased 3→3.5mm (both per this request). Net effect on everything below the map: shifted up 0.5mm (map shrinking by 1mm minus the gap growing by 0.5mm). Widens the overflow margin slightly rather than spending it, so the `remarkOnCoverage` threshold (`>4` products, since 1.20) needed no further changes. Applied to both live `.mrt` files after backup, verified via the render harness against real job 106 and job 108 data. |
| 1.23 | 2026-08-20 | Investigated a product observation that Mission Overview still showed visible blank space below the Weather table on job 108, questioning whether Remark could have stayed there instead of relocating. Verified via the render harness with the real 6-product job 108 data and its actual (2-line) remark text, forcing relocation off: the blank space is leftover slack *after* Remark is already excluded, not room additional to it — 6 products still genuinely overflows onto a 9th page if forced to stay. However, re-testing the boundary against the current (post-1.21/1.22) map geometry found the safe cutover had moved: 5 products now fits Mission Overview cleanly (it required relocation under 1.20's `>4` threshold, set when the map was still 110mm and margin tighter), while 6 still genuinely overflows. Loosened `mission.remarkOnCoverage` back to `products.length > 5` — matching the original product request — now that 1.21/1.22's map-height reductions recovered enough margin to support it. Reverified both boundary cases via the harness. |
| 1.24 | 2026-08-20 | Fixed a gap in the `remarkOnCoverage` check that 1.23 didn't cover: a long *remark* can overflow Mission Overview independently of product count. Caught on job 108 with a (data-level) duplicated 3-line remark, which overflowed at only 5 products — a count the 2-line-remark case had just confirmed as safe. `mission.remarkOnCoverage` now weighs both factors as a shared "growth budget": each product row beyond the first, and each estimated wrapped remark line beyond the first, counts as one unit against a budget of 5, calibrated against three real/verified data points — (5 products, 2 lines)=safe, (6, 2)=overflow, (5, 3)=overflow — all land exactly on that boundary. Remark line count is estimated from character length ÷ 100 (deliberately conservative: the only two real calibration points are ~147 chars→2 lines and ~296 chars→3 lines; Stimulsoft's actual text-wrap isn't reproduced, so this is biased toward over-estimating lines, which only costs an occasional early relocation rather than a missed one). Verified via the render harness across four cases (5p/2-line, 6p/2-line, 5p/3-line-duplicated, 4p/2-line) — all render correctly with no overflow. |
| 1.25 | 2026-08-21 | Fixed "Farm:" rendering blank on both Mission Overview and Zone Detail despite `mission.farm`/`zones[].farm` being present and correct in the generated `rptDS.json` (confirmed via direct inspection of a live-cached job 108 generation) — root cause was in the `.mrt` templates, not the datasource: Stimulsoft binds a `{table.column}` expression against the Dictionary's own design-time-declared column list for that table, not against whatever fields the loaded JSON actually contains at runtime. The original Farm-field work (1.13) added the `pnlFarm`/`txtFarm`/`pnlZnFarm`/`txtZnFarm` components and their `GlobalizationStrings` entries, but never registered `farm` as a declared column on the `mission` or `zones` Dictionary data sources, so `{mission.farm}`/`{zones.farm}` silently resolved to nothing — reproduced offline via the render harness (blank with real data, still blank after swapping in an obviously-distinct test value, confirming the expression bound to nothing at all rather than an empty field), then fixed by adding the missing `farm` column declarations to both tables. Also fixed a smaller cosmetic issue noticed alongside: `txtFarm` was missing the `HorAlignment: "Right"` every other Mission Facts value field has (`txtZnFarm` was already correctly left-aligned, matching Zone Detail's own convention). Applied to both live `.mrt` files after backup, reverified with real job 108 data on both pages. |