agmission/server/docs/ADVANCED_REPORTS_IMPLEMENTATION_PLAN.md

123 lines
34 KiB
Markdown
Raw 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 — Implementation Plan (Phase 1)
**Version:** 1.0
**Date:** July 9, 2026
**Status:** Draft — derived from the approved planning set
**Related Documents:** `ADVANCED_REPORTS_FEASIBILITY.md`, `ADVANCED_REPORTS_FUNCTIONAL.md`, `ADVANCED_REPORTS_NON_FUNCTIONAL.md`, `ADVANCED_REPORTS_API.md`, `ADVANCED_REPORTS_PROPOSAL.md`
---
## 1. Approach
Build the Advanced Application Report on the existing report pipeline: the server computes analytics, renders map images, and writes a datasource; the client Stimulsoft viewer renders and exports. One analytics engine feeds every page (FR-5.4); content options are applied by shaping the datasource, not by swapping templates.
Guiding principles:
- **Pure-function analytics core** — testable without HTTP, Mongo, or Puppeteer (NFR-6.3).
- **Legacy untouched** — no shared-code change may alter `preAppReport`/loadsheet behaviour (NFR-5.1).
- **`ApplicationDetail` is read once per report** (single pass, streaming cursor, projected fields only — NFR-1.2/1.4).
- **One Chromium instance** per report for all captures (NFR-1.3).
## 2. Deliverables map
| # | Deliverable | New/Changed files |
|---|---|---|
| D1 | Analytics engine | `helpers/report_util.js` (new) |
| D2 | Endpoint + datasource builder | `controllers/advanced_report.js` (new), `routes/job.js` (route), `model/job.js` (report-contents options persistence) |
| D3 | Map page variants + multi-capture | `public/sprayMap.html` (variants), `helpers/web_util.js` (browser reuse) |
| D4 | Template + validation | `reports/app_advanced.mrt` (authored in the Stimulsoft designer), `scripts/validate_advanced_report_template.js` (new) |
| D5 | Frontend wiring | client repo: Report Settings "Report Contents" panel, advanced-report option, viewer call |
| D6 | Tests + offline harness | `tests/` fixtures + unit/integration tests, offline Stimulsoft harness |
| D7 | Rollout | backfill verification, template deploy to `REPORT_DIR`, release notes |
Sequencing: **D1 → D2 → (D3 ∥ D4) → D5 → D6 → D7.** D3 and D4 are independent once D2 fixes the datasource shape. D6 grows alongside every deliverable (D1 unit tests land with D1).
## 3. D1 — Analytics engine (`helpers/report_util.js`)
Pure functions over point arrays; no I/O.
1. **Line segmentation** — group `ApplicationDetail` points by `llnum`; `sprayStat == 3` marks line start, spray-on = `sprayStat ∈ {1, 3}` (pattern: `getSprayOnSegments`, `controllers/job.js:604`).
2. **Zone assignment** — point-in-polygon (turf) of each line's points against `job.sprayAreas`. A cheap sampled check first asks whether the line's points touch more than one real zone at all; if not, majority wins as before (FR-5.2). If it genuinely straddles two zones, the line is split into one segment per zone actually crossed (via a full, unsampled per-point pass), instead of handing the whole line to whichever zone the sample favors — fixes a real case where a small zone lost most of its coverage credit to a much larger neighbor across a shared boundary (FR-5.2 refinement, `helpers/report_util.js`: `zoneOfPoint`/`straddlesMultipleZones`/`splitPassByZone`). The single GPS interval that actually crosses the boundary is deliberately not counted in either segment's length/area/volume — a documented, bounded (one interval per crossing) trade-off rather than added complexity to split it between two zones.
3. **Per-line stats** — start time, spray time, length (`geoUtil.distance()` — **km**), avg speed (mean `grSpeed` over every `sprayStat>0` record, `sprayStat==3` marker included — matches the actual legacy `avgSpraySpeed` code, `workers/job_worker.js:1470-1472`; `AGGREGATED_FIELDS_CALCULATION.md`'s prose description of this rule is wrong — it conflates the marker exclusion that applies to spray-*time*, not speed — verify against the real code, not that doc, if this ever comes up again), area = length × swath, app rate, avg |xTrack|, turn time (gap to next line, attributed to the zone the *preceding* pass was assigned to — a reused line number in a different zone must not inherit another zone's turn; pattern: turn-time loop `workers/job_worker.js:1486`).
4. **Zone roll-ups** — sprayed area, coverage %, volume, flight/spray time, avg turn time, avg height, avg XT, avg flow rate (mean `lminApp` — completely new calculation, degrade when flat 0).
5. **Mission totals** — sums/weighted averages of zone values (must equal page-1 figures exactly, NFR-3.3); ferry time/distance = flight spray. Total Volume / Avg App Rate dash at the mission level under the same no-flow-data condition zones/lines already do — no configured-rate (`job.appRate × sprayedArea`) fallback is substituted, since that figure is a pre-flight target, not a measurement, and would silently break the mission ≡ Σ(zones) invariant this section exists to guarantee (a manual `rptOp.actualVol` override is unaffected, being user-entered rather than assumption-based).
6. **Planned areas** — turf area from polygon geometry, net of any intersecting `job.excludedAreas` (mirrors `jobUtil.calcTTSprayAreas`, matching the job's own `Job.ttSprArea`); `sprayAreas[].properties.area` is used as a shortcut only when there are no exclusion zones to net out (it's absent in most live data anyway, and can't be trusted once there's overlap to subtract). Mission Coverage % sums each zone's sprayed area **capped at that zone's own planned area** before dividing by total planned area — an overlapped/oversprayed zone must never numerically stand in for a zone that was never sprayed at all (that would let Coverage read 100% while `zonesSprayed < zonesTotal`, i.e. zones were plainly left untouched). Display then also caps at 100.0% as a defensive floor, though the per-zone cap already guarantees the ratio can't exceed it. A zone's *own* coverage % (shown on its Zone Detail page / Mission Coverage Grid card), by contrast, is **not** capped — real overspray from swath overlap, turns, or re-flown sections is normal and legitimate, and can genuinely exceed 100% of that zone's own plan; showing the true figure is more useful than flattening it to a look-alike "100%". When a manual Report Settings override supplies the planned/sprayed totals directly (no per-zone breakdown to cap against), that fallback ratio is likewise **not** capped, for the same reason — the override's own numbers can legitimately claim more was sprayed than planned, and hiding that behind "100%" reproduces the exact problem this section exists to avoid.
7. **Weather**`jobUtil.getDataWeatherInfoPerField(fileIds)` (`helpers/job_util.js`), added in place of the original `getDataWeatherInfo` reuse: each of windSpd/windDir/temp/humid is validated and dashed **independently**, so one implausible sensor field (e.g. a stuck temperature reading) doesn't blank the other three the way the legacy all-or-nothing `$match` filter still does; manual `job.weatherInfo` override takes precedence when set. `temp` follows the job's `measureUnit` (°F/°C) like every other quantity, matching legacy's own formatting.
Unit-test fixtures (with D1): typical multi-zone job, no-flow-controller job (`lminApp` = 0), SatLoc-style job (no xTrack/turn data), unsprayed zone, single-zone job, boundary-straddling line (majority-zone case: leaves a zone into empty space, stays whole), genuine cross-zone-boundary line (splits into one segment per zone actually crossed).
## 4. D2 — Endpoint + datasource (`controllers/advanced_report.js`)
1. `POST /preAdvancedReport` in `routes/job.js`, same auth middleware as `preAppReport` (NFR-4.1). Returns `{ rid, path, c }` (FR-1.1).
2. Controller flow (mirrors `preAppReport_post`): load job with populated refs → persist Report Settings incl. Report Contents selections (`job.rptOp` pattern, FR-7.5) → stream `ApplicationDetail` by the job's `fileId`s with field projection (NFR-1.2) → run D1 engine → render maps via D3 → write `rptDS.json` → select template.
3. Datasource shape (all display values pre-localized strings, FR-1.3):
- `mission` — header/info block, KPI tiles, statistics, generation date. `mission.farm` (added post-Phase-1; API changelog 1.13) sources `job.farm`, the same field the legacy report already labels "Farm:".
- `zones[]` — per-zone info + stats + map image refs; empty-state zones carry `` placeholder values (FR-4.6); filtered per Report Contents options. `zones[].farm` (added alongside `mission.farm`) repeats the same mission-wide value on every zone, the same pattern already used for `zones[].crop`/`product`.
- `lines[]` nested per zone — flight-line table rows; single `` placeholder row for unsprayed zones; omitted when Flight Line Statistics is off, purely per the caller's own Report Contents choice — this table is never role-gated. The report's map captures, separately, omit the ferry/flight-path polylines (spray lines are unaffected) for a role not authorized to view flight paths (`flightPathViewRoles`, `helpers/constants.js` — the same `APP`/`APP_ADM`/`OFFICER` set the client's Job Map gates its Flight Paths overlay behind) — this is scoped to the map imagery only, mirroring the Job Map's own scope (the overlay there is a map layer, not a data table).
- `products[]`, `weather` (suppressed when unavailable), `coverageCards[]` (all zones, always).
4. Template selection: `app_advanced_<applicatorId>.mrt` else `app_advanced.mrt`; applicator id sanitized to hex ObjectId (NFR-4.2).
5. Structure the generation function so a worker can call it without the HTTP layer (NFR-2.3); a simple in-process counter limits it to max 2 concurrent generations (NFR-2.2).
6. Per-phase pino logging: query, data aggregation, each capture, datasource write (NFR-7.1).
7. **Generation cache (implemented post-Phase-1 — resolves Open Item below; API changelog 1.11)**: before touching `ApplicationDetail` or Chromium, hash everything that would actually change the output — zone/exclusion geometry, `job.rptOp`/`useCustWI`/`weatherInfo`, applicator, an imported-data fingerprint (each `App.updateDate`, since `ApplicationDetail` rows carry no timestamp of their own), Report Contents, `dataOp`, language, and the requester's flight-path-visibility role — against a `Job.advRptCache` hash saved from the previous generation. On a match, and only if that prior run's `rptDS.json` is still on disk, return the previous `{rid, path, c}` directly, skipping the analytics engine and every map capture entirely. Template selection is always recomputed fresh either way, since a `.mrt` file can be added/removed independently of anything that would invalidate the cache.
## 5. D3 — Maps (`public/sprayMap.html` variants + `helpers/web_util.js`)
1. Extend `web_util` to open one Chromium instance and capture multiple pages/states per report (NFR-1.3).
2. Mission map variant: fitBounds over all zones; numbered markers + zone/field names + acreage labels; legend/scale/north arrow. **Mode switch** (FR-2.3.3): compute zone pixel footprint at fitted zoom — below threshold (~25 px) render locator badges (`divIcon`, zone numbers) instead of polygons (FR-2.3.2). Spray corridors and flight-path lines are **not** shown on this top-level overview capture, for any user role — plain zone polygons + number/name/area labels only (changed this session); that detail is already shown per-zone on the Mission Coverage thumbnails and Zone Detail maps (item 3 below), and `missionOverview()` explicitly hides every `_zoneIdx`-tagged layer before this capture is taken.
3. Zone detail variant: per-zone fitBounds, boundary + spray lines + dashed ferry lines, neighbouring zones faded into the background; unsprayed zones render boundary + ferry only.
4. Background toggle: when *Hide Map Background* is selected (FR-7.4), all variants skip the satellite tile layer and render on the plain dark-green background used in the mockups (a fixed CSS background on the map container) — no tile downloads during capture.
4. Thumbnails: **the same per-zone `focusZone` fit/refit used for that zone's own Zone Detail page**, but captured as its own dedicated file (`zone_thumb_<n>.jpg`, distinct from Zone Detail's `zone_<n>.jpg`) — **skipped entirely above 12 zones** (FR-3.5) or when zone pages are excluded (Report Contents). Revised this session (`controllers/advanced_report.js`, `public/sprayMapAdvanced.html`) away from an earlier approach that cropped a rect out of the single all-zones capture (sized via each zone's pixel footprint at the shared mission-wide zoom): that crop's native resolution and framing were both at the mercy of whatever zoom the mission view had to use to fit every zone at once, which produced inconsistent thumbnails across jobs — blurry/thick boundaries for small zones next to large ones (Job #105), and crops dominated by ferry-track clutter for zones with a widely-separated sibling forcing a very zoomed-out mission view (Job #108). Reusing `focusZone`'s independent per-zone `fitBounds` gives every zone its own correctly-scaled capture regardless of the other zones' size or spread, eliminating the need for the crop/aspect-ratio/minimum-size-floor logic the earlier approach required. `applyZoneFocusStyle` (also added this session) shows only the focused zone's own spray corridors and flight-path segments — both now carry a `zoneIdx` tag (mirroring each other) — fading/hiding neighbouring zones' data instead of showing everything in range. The thumbnail capture briefly bumps the zone/exclusion polygon boundary stroke via `window.setZoneStrokeWeight` (a later revision this session) before its shot, resetting it back for the Zone Detail shot — the Mission Coverage card embeds the same source image at a much smaller physical size (~57×36mm vs Zone Detail's ~190×135mm), so a shared fixed-pixel stroke width would print roughly 3x thinner on the card than on the Mission Overview/Zone Detail pages; the two captures could not stay a single shared file once this diverged.
5. Zone capture failure → placeholder image + log, report continues; mission map failure → request fails (NFR-3.1).
6. **Capture timing (revised post-Phase-1; API changelog 1.11)**: the fixed post-`fitBounds` settle delays described above (and 1.10's later "cancel any pending settle timer" patch) were replaced with an adaptive mechanism (`waitForMapIdle`/`waitForBasemapReady`, `sprayMapAdvanced.html`) that waits for each basemap's own authoritative "finished" signal instead of a delay of any kind: Google's real `tilesloaded` event for the premium satellite basemap (captured once via GoogleMutant's one-time `spawned` event and reused for every later refit — `initMapBaseLayer`, `utils.js`, now returns its layer reference(s) for exactly this) and Leaflet's own repeatable `load` event for the plain Esri/OSM layers, falling back to a DOM-mutation watch (filtered to `<img>`-specific activity, so Leaflet's own non-image grid-layer scaffolding can't fool it into declaring "done" before a single tile has actually arrived) only when neither basemap reference is available. A `window.loaded` property gate absorbs `initMapBaseLayer`'s own premature write — tied to `spawned`, not to when tiles actually render — so it can't win the race against the real signal. Fixed a related bug found on a real 5-zone job: refocusing the *same* zone a second time for its Mission Coverage thumbnail (identical camera position to the Zone Detail shot just captured) never gets a new tile-load event at all, since an unchanged view requests no new tiles; `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 full 12-capture batch (map + 5 zone details + 5 thumbnails) went from 602s wall-clock with 10 of 12 shots failing outright (idling out to the 60s per-shot timeout) down to ~5.4s with all 12 succeeding — this was the actual cause of the blank/missing Mission Coverage thumbnails seen in production, not a template or datasource defect. **Explored and reverted, not shipped**: generating the Mission Coverage thumbnail by compositing the already-captured Zone Detail frame (redrawing just the heavier boundary stroke on top via an in-page canvas/SVG overlay, screenshotted directly) instead of a second live `focusZone` capture — fully implemented (`window.renderZoneThumbOverlay`/`clearZoneThumbOverlay`, a new `shot.fn` escape hatch in `web_util.js`'s `webShotBatch`) and verified correct in isolation, then reverted at product's request; the same-zone-refocus fix above addresses the performance/reliability problem this was originally meant to solve, so the added complexity wasn't kept.
## 6. D4 — Template (`reports/app_advanced.mrt`, authored in the Stimulsoft designer)
1. Three page designs authored manually in the embedded Stimulsoft designer: Mission Overview, Mission Coverage, Zone Detail (master band per zone, flight-line table as StiPanel-wrapped child band). Mission Coverage's grid density now scales in three tiers by zone count, mutated on the loaded Stimulsoft report object at render time (`report.component.ts`, not baked into the `.mrt` as separate layouts) rather than the earlier binary "grid ≤12 / compact table >12" split: **≤6 zones** get 2 columns with large cards (the template's own baked-in default, card-to-card gap unified to a consistent 6mm both horizontally and vertically); **7-12 zones** get 3 columns at the original card size; **>12 zones** get a compact, map-free text-only grid (thumbnail collapsed to zero height, same 60mm column width as the 7-12 tier so its already-correct text-box widths are reused rather than re-derived). Verified via a Puppeteer harness driving the real Stimulsoft engine — confirmed `Columns`/`ColumnWidth`/component `left`/`top`/`width`/`height` are plain settable properties post-load and the render actually reflects the mutation, not just the property read-back — before landing in `report.component.ts`. A follow-up to also split Sprayed/Planned into separate fields and add Crop/Volume Applied to the >12-zone card was implemented, verified, and then reverted at product's request pending a team-lead decision on the compact tier's page-space usage (see the >12 tier's still-noticeable blank space below a short zone list, e.g. 13-14 zones) — not currently in either template or `report.component.ts`. Zone Detail's Zone Info panel gained a Product row (mission-wide active-ingredient list, positioned after Crop) and its Flight Statistics box was reorganized into two even columns (Start/End Time, Flight/Spray Time, Avg Turn Time on the left; Avg Speed/Height/XT Error/App Rate/Flow Rate on the right) with uniform 2mm padding. Mission Overview's Remark line, together with the fixed page-wide vertical rhythm (Mission Facts/Map/KPI-cards/Mission-Statistics/Products/Weather gaps), needed rework after discovering `MissionBand`'s lack of `CanGrow` let a job with more products/weather rows than the template was originally sized for push Remark past the band's own declared height and force an unwanted near-empty continuation page — fixed with `CanGrow: true` plus tightened gaps (see API doc §12 changelog 1.9 for exact values); relocating Remark onto the Mission Coverage page instead was prototyped and verified working as an alternative fix, then not shipped in favor of keeping it on Mission Overview. All three pages' header banners also got their logo/title margins tightened (8mm→6mm) and the "Advanced Application Report" title enlarged and vertically re-centered against the logo's actual optical center.
2. Validation script `scripts/validate_advanced_report_template.js` (NFR-5.2/6.1), run after every designer save and before deploy. Checks: no empty `{}` collections in the `.mrt` JSON; `GlobalizationStrings` for en-US / pt-PT / es-ES with non-empty Items targeting existing components; unique component names; every band's `DataSourceName` / `MasterComponent` / `DataRelationName` resolves against the Dictionary; DataBands nested inside DataBands are StiPanel-wrapped; every `{table.column}` expression references a declared Dictionary column.
3. Section suppression via empty datasets; dash placeholder rows come from the datasource, not template logic.
4. Committed `.mrt` is the source of truth (NFR-6.1). Footer `Created <date>` + `page/totalPages`; header band per page type.
5. **Stimulsoft band positioning gotcha (discovered 2026-07-31, worth knowing before touching any page-level spacing)**: a band's own `ClientRectangle` Y coordinate is *not* what places it on the page — the render engine stacks each band immediately after the actual rendered height of whatever band precedes it. Only *static* (non-band) child components — e.g. a plain `StiText`/`StiPanel` like `pnlZnZone` — honor their own authored relative Y offset once their parent band's position is resolved; a nested *band* (like the Flight Line Statistics header+data band pair) discards its own and its ancestors' declared offsets entirely when it resumes on a continuation page, resuming flush against whatever precedes it. Practical upshot: the gap below a repeating page header (`StiPageHeaderBand`) is controlled purely by that header band's own `Height` versus its visible content's height — not by the following content band's `Y` — and that same header `Height` simultaneously controls the gap on both the page's first occurrence *and* any later continuation page. Each page type (`ReportTitleBand1`+`MissionBand`, `PageHeaderBand2`+`coverageBand`, `PageHeaderBand3`+`ZoneBand`) needs this calibrated the same way for consistent gaps across pages. Separately, a data band's declared height is only respected if `CanShrink` is `false` — with `CanShrink: true` (the default used on `coverageBand`), the band silently collapses back down to fit its tallest child regardless of the declared height, which is why a naive "just make the band taller" fix for row spacing has no visible effect until `CanShrink` is turned off.
6. **Farm field + Mission Statistics rebalance (implemented post-Phase-1; API changelog 1.13)**: a new "Farm:" row was added to the Mission Facts panel on Mission Overview (right after "Job:", before "Crop:") and to the Zone Info panel on Zone Detail (right after "Zone:", before "Crop:") — `pnlFarm`/`pnlZnFarm` panels each containing a label `StiText` and an Expression-type value `StiText` bound to `{mission.farm}`/`{zones.farm}`, following the exact same label+value StiPanel shape as the existing rows in each panel. Every component positioned below the new row within its own panel and within the page's overall vertical stack was shifted down 5mm (one row height) to preserve existing spacing exactly — computed from each panel's established row-height convention rather than eye-balled, then verified against real job data via a purpose-built offline Stimulsoft-rendering harness (loads the live `.mrt` + a real job's `rptDS.json` through the same `StiReport`/`StiViewer` sequence `report.component.ts` uses, screenshotted via Puppeteer) before either live template file was touched. Separately, per product request: Mission Statistics' "AppRate:" row was removed (the mission-level configured/overridden rate, `mission.appRate` — distinct from the flow-derived `mission.avgAppRate`, which stays and is unaffected) and the remaining rows rebalanced from an uneven 5/4/4 column split into three even 4-row columns — column 3's Avg AppRate/Avg Flow Rate/Total Volume each moved up one row into AppRate's vacated slots, "Ferry Time" moved from column 1 into column 2's newly free 4th row, and "Swath Width" moved from column 2 into column 3's newly free 4th row; column divider lines shortened to match the new uniform 4-row height. `mission.appRate` itself is still computed and present in the datasource, just no longer rendered. New `GlobalizationStrings` added for `lbFarm.Text`/`lbZnFarm.Text` across en-US/pt-PT/es-ES ("Farm:"/"Fazenda:"/"Finca:"). Applied directly to both live `REPORT_DIR` `.mrt` files (base + per-applicator override) after taking backups; the branch's own version-controlled `.mrt` copies under `reports/` remain out of sync with those live files, per the pre-existing, deliberately-deferred branch-vs-trunk reconciliation noted elsewhere in this doc.
7. **Mission Duration row removed (implemented post-Phase-1; API changelog 1.14)**: dropped from the Mission Facts panel as redundant with "Total Flight Time:" already shown in Mission Statistics. `pnlDuration` deleted, `pnlMissionFacts` shrunk by one row height, and everything below it in the page's vertical stack shifted back up 5mm — exactly reversing the downward shift item 6's Farm-row insertion required, so the rest of the page (Products, Weather, Remark) is untouched. `mission.duration` stays in the datasource unused, per the same precedent as `mission.appRate` in item 6. Verified the same way, against the live override file and real job data.
8. **Mission Statistics bottom whitespace tightened (implemented post-Phase-1; API changelog 1.15)**: items 6 and 7 above left the panel's 4 content rows (20mm) inside an unchanged 30mm-tall box. `pnlMissionStats` height reduced to 25 (a consistent 3mm margin above and below the content), Products/Weather/Remark shifted up 5mm to close the gap. Verified the same way.
9. **Remark relocated to Mission Coverage page when it would overflow (implemented post-Phase-1; API changelog 1.16)**: a long product list (real case: job 106, 6 products) can push Remark past Mission Overview's fixed page budget, spilling it alone onto a near-empty continuation page. Rather than emulating Stimulsoft's text-layout engine to predict the overflow exactly, `mission.remarkOnCoverage` (`controllers/advanced_report.js`) is a simple deterministic proxy: `true` when `products.length > 5`. Both live `.mrt` files gained a mirrored Remark row (`pnlRemark2`/`lbRemark2`/`txtRemark2`) as a standalone component on the Mission Coverage page, next to (not nested in) `coverageBand`. `report.component.ts`'s existing pre-render mutation function (§5 D3 item 6 / §6 D4 item 1) now also toggles `.enabled` on whichever Remark row applies and, for the relocated case, computes `pnlRemark2`'s absolute `top` from `coverageBand.top` plus the expected grid height (`Math.ceil(zoneCount / columns) * rowHeight`, same per-tier constants as the coverage-grid-density mutation) — since a plain `StiPanel` on a page doesn't auto-stack after a preceding repeating data band the way two Bands would. Verified via the render harness for both the ≤5-product and >5-product cases against the live override `.mrt` and job 106's real data. This one requires a client rebuild to take effect, unlike the live-file-only template edits in items 6-8.
10. **Mission Coverage card spacing tightened (implemented post-Phase-1; API changelog 1.17)**: the ≤6-zone tier's card layout (the `.mrt`'s own baked-in default, §6 D4 item 1) used a looser rhythm than the rest of the report — a 6.23mm name row plus two 4.98mm rows with ~1.25mm gaps between them, instead of the 5mm-contiguous-row convention used in Mission Statistics and Flight Statistics. Retiled the three text rows (zone name, Sprayed/Planned, Coverage %) to 5mm each, back-to-back. Margins went through three rounds of feedback before landing on explicit values: 4mm left/right, 3.75mm bottom (1mm → 4.63mm-to-match-left/right → 3.5mm → 3.75mm as the final call). `pnlCard`/`coverageBand`'s heights shrunk by the net amount saved, preserving the existing gap between grid rows; `cardThumb` itself was never touched. The 7-12 and >12 tiers (already hand-tuned tight in `report.component.ts`) were untouched. Verified via the render harness, before/after, against real job data.
11. **Remark-relocation regression fixed (API changelog 1.18)**: item 9's `pnlRemark2` positioning read `coverageBand.height` as the row pitch, which drifted stale the moment item 10 retuned that height, and turned out to be an unreliable proxy regardless — `CanShrink` collapses the band's declared height to the card's real rendered size at render time, so even a fresh copy overestimates the true pitch. On job 108 (5 zones, 6 products) this pushed Remark past the visible page, reproducing the exact overflow bug item 9 was meant to fix. Fixed by reading `pnlCard.height` instead, the same property the grid-density mutation already sets per tier — can't drift out of sync with whatever tier is active. Verified against job 108's real data via the render harness.
12. **Remark anchored to the footer instead of the grid (API changelog 1.19)**: per product feedback, relocated Remark should sit just above the page footer rather than directly below the grid — a low-row-count tier (job 96's 9-row >12-zone grid, job 109's 3-row 7-12-zone grid) otherwise leaves a large, inconsistent gap. Reads `PageFooterBand2.top` dynamically (differs between the base and per-applicator `.mrt`) and reserves a fixed 20mm text budget + 3mm gap above it, falling back to right-after-the-grid only when the grid already extends past that point (a near-full 7-12-zone tier). Verified against jobs 108, 96, and 109's real data via the render harness.
13. **Mission Overview map enlarged, overflow threshold retuned (API changelog 1.20)**: `missionMap` height 104→110mm, everything below it on the page shifted down 6mm to match. This consumed 6mm of the same page-space margin item 9's overflow fix depends on; reverified via the render harness (job 108 data, trimmed to 2-6 products) that the safe cutover moved from 5 to 4 products with the taller map. Retuned `mission.remarkOnCoverage`'s threshold from `>5` to `>4` products accordingly and reconfirmed the previously-safe 5-product case now relocates cleanly instead of overflowing, without disturbing the real 6-product job108 case items 9/11/12 already cover.
14. **Mission map height corrected 110→107mm (API changelog 1.21)**: recovers 3mm of item 13's margin spend; everything below the map shifted up 3mm to match. Left the `remarkOnCoverage` threshold at `>4` rather than loosening it back — over-relocating costs nothing, unlike the under-relocation item 13 had to fix. Reverified the 4-product boundary and the real 6-product job 108 case via the render harness.
15. **Mission map height 107→106mm, map-to-KPI gap 3→3.5mm (API changelog 1.22)**: net -0.5mm shift on everything below the map (widens the overflow margin slightly, so no threshold change needed). Verified via the render harness against real job 106 and job 108 data.
16. **`remarkOnCoverage` threshold loosened back to >5 (API changelog 1.23)**: 1.21/1.22's map-height reductions recovered enough margin that 5 products now fits Mission Overview cleanly again (it needed relocation under 1.20's `>4`, set when the map was still taller/tighter); 6 products still genuinely overflows if forced to stay, reverified with job 108's real 2-line remark text via the render harness. A visible blank gap below Weather on job 108's Mission Overview render turned out to be leftover slack *after* Remark was already excluded, not room additional to it.
17. **`remarkOnCoverage` now weighs remark length too (API changelog 1.24)**: caught a gap where a long remark alone (job 108's duplicated 3-line remark) overflowed at only 5 products — a count item 16 had just confirmed safe for the usual 2-line case. Modeled as a shared budget: each product row and each estimated remark line beyond the first costs one unit, calibrated against three real data points (5p/2-line=safe, 6p/2-line=overflow, 5p/3-line=overflow) that all land on a budget of 5. Line count estimated from character length ÷ 100, deliberately conservative. Verified via the render harness across all four boundary cases.
18. **"Farm:" rendering blank, fixed (API changelog 1.25)**: the datasource was correct (`mission.farm`/`zones[].farm` verified present in a live-cached job 108 generation) — the bug was that 1.13's original Farm-field work never registered `farm` as a declared column on the `mission`/`zones` Dictionary data sources in the `.mrt`. Stimulsoft binds `{table.column}` expressions against that design-time-declared schema, not the runtime JSON, so the expression silently resolved to nothing regardless of the actual data. Fixed by adding the missing column declarations to both tables; also fixed `txtFarm` missing the `HorAlignment: "Right"` its sibling Mission Facts fields have. Reverified on both Mission Overview and Zone Detail with real job 108 data.
## 7. D5 — Frontend (client repo)
1. Report Settings dialog: add right-side **Report Contents** panel — Include All Zone Detail (default on), nested Sprayed Zones Only (default off), Include Flight Line Statistics (default on), Hide Map Background (default off), info tooltips (FR-7.4); restore last selections per job.
2. Advanced Report as a report option alongside the legacy report; on Preview call `preAdvancedReport` and hand `{rid, path}` to the existing viewer unchanged.
## 8. D6 — Testing & verification
1. D1 unit tests over fixtures (all FR-8 degradation rows covered, NFR-3.4).
2. Cross-page consistency test: mission totals ≡ zone roll-ups (NFR-3.3).
3. Offline Stimulsoft harness (file:// + `stimulsoft.reports.pack.js`) loading the real `.mrt` + generated `rptDS.json` — reproduces viewer load/localize/render without the app (NFR-6.2); Trial watermark acceptable in tests.
4. Integration run against a live-like multi-zone job; visual check of all three page types, both map modes, >12-zone compact layout, both unit systems, all three cultures.
5. Performance measurement against NFR-1.1 (~35 s per 10 zones; ~15 s typical 3-zone job) with per-phase timings from NFR-7.1 logs.
## 9. D7 — Rollout
1. Verify production aggregate coverage (`avgXtError`, `avgSpraySpeed`, `totalFlightLength`) on recent Applications; re-run `scripts/migrate_applications.js` only if gaps found (NFR-8.1).
2. Deploy `app_advanced*.mrt` to the environment's `REPORT_DIR` (may be outside this repo — NFR-6.4).
3. Release notes: flow-rate fields require a flow controller; SatLoc-sourced applications omit XT/turn statistics (NFR-8.2).
## 10. Open items
- **F-OQ-1 Page orientation** (portrait-only vs landscape variant) — blocks D4 template freeze; portrait assumed until the PO decides.
- **F-OQ-2 Compact coverage layout threshold** (more than 12 vs 12-and-above; threshold value) — affects D3 thumbnail logic and the D4 coverage page; "more than 12" assumed until the PO decides.
- **Regeneration reuse/caching** for repeat downloads of unchanged reports — **Implemented** post-Phase-1 (see D2 item 7 above; API changelog 1.11), not deferred after all.