agmission/server/docs/ADVANCED_REPORTS_API.md

65 KiB
Raw Blame History

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

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

{
  "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 (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.

{
  "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.type0 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.remarkjob.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:

{ "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 fileIds — 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).

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["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

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 TypeJob, Total DurationMission Duration, KPI COVERAGECOVERAGE %, Avg App. RateAvg 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/602/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.updateDateApplicationDetail 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.