The manager's monthly NLC report (design 10) #13

Merged
hermes-agent merged 4 commits from feature/manager-nlc-report into main 2026-10-28 20:30:21 +00:00
Owner

The Contest Manager publishes the month's outcome as a fixed-shape document. They sent their current PDF (Uitslag_NLC_249_voor_CQ-PA.pdf); this PR makes the robot produce that document.

What it looks like

Built from the PDF examined in coordinate space, so the layout is the manager's real one, not a reading:

  1. Nederlandse Locatorcontest nr. <nr> <month> <year> — Dutch month name, editor number supplied at publish time (?nr=249).
  2. Uitslag NLC — per section, rank-ordered, in the manager's own column order: Call | aantal banden | QSO | Multipliers | Score | VRZA afd. | Afd. Punten, headings in the reglement's wording ("Sectie A Multi band – Multi operator").
  3. Tussenstand Afdelingsbeker — per afdeling: the month's entrant calls and count.
  4. Tussenstand NLC — year-so-far per section, reusing build_yearly_standing_data so the participation/drop rule is never reimplemented.

/manager/contest/{id}/nlc-report?nr=249 renders it print-ready; the manager exports the PDF from the browser print dialog, like their current LibreOffice workflow. The dashboard links it. Rendered demo from the live stack: docs/examples/nlc-report-demo.html.

What is deliberately NOT produced

  • The prose. The editorial text and closing signature are the manager's own voice — the robot does not write manager copy. Two clearly marked placeholder blocks instead.
  • The numbers the document does not explain. "Afd. Punten" per entry and the beker "TOTAAL" (342 for PI4ZWN at 7 listed inzenders) do not follow from the sample in any derivation I could find (rank, score, entrant count all fail). They render as placeholders with a note, and the questions are filed in docs/open-questions-contest-manager.md (#9 area: puntenregeling, clubstation+afdeling cell, E/F section headings, editor numbering).

Privacy (R25) holds: the report exposes only callsign, section, afdeling, band count, QSO count, multipliers and score — a test asserts no email/address/operator name reaches the HTML.

Tests

305 (was 296). New: section grouping and rank order, band/QSO/multiplier counts from the stored score basis (including an ineligible-band entry counted under -), contest-number pass-through, beker per-afdeling grouping with placeholder totals, year-so-far equals the yearly builder, privacy (no @example, no address), manager-only access, empty-month placeholders, unknown contest redirect, dashboard link. Verified live on the running stack; entry rows match the ingested corpus (PD1SCO 2 bands / 5 QSOs / 25).

The Contest Manager publishes the month's outcome as a fixed-shape document. They sent their current PDF (`Uitslag_NLC_249_voor_CQ-PA.pdf`); this PR makes the robot produce that document. ## What it looks like Built from the PDF examined in coordinate space, so the layout is the manager's real one, not a reading: 1. `Nederlandse Locatorcontest nr. <nr> <month> <year>` — Dutch month name, editor number supplied at publish time (`?nr=249`). 2. **Uitslag NLC <nr>** — per section, rank-ordered, in the manager's own column order: `Call | aantal banden | QSO | Multipliers | Score | VRZA afd. | Afd. Punten`, headings in the reglement's wording ("Sectie A Multi band – Multi operator"). 3. **Tussenstand Afdelingsbeker** — per afdeling: the month's entrant calls and count. 4. **Tussenstand NLC** — year-so-far per section, reusing `build_yearly_standing_data` so the participation/drop rule is never reimplemented. `/manager/contest/{id}/nlc-report?nr=249` renders it print-ready; the manager exports the PDF from the browser print dialog, like their current LibreOffice workflow. The dashboard links it. Rendered demo from the live stack: `docs/examples/nlc-report-demo.html`. ## What is deliberately NOT produced - **The prose.** The editorial text and closing signature are the manager's own voice — the robot does not write manager copy. Two clearly marked placeholder blocks instead. - **The numbers the document does not explain.** "Afd. Punten" per entry and the beker "TOTAAL" (342 for PI4ZWN at 7 listed inzenders) do not follow from the sample in any derivation I could find (rank, score, entrant count all fail). They render as placeholders with a note, and the questions are filed in `docs/open-questions-contest-manager.md` (#9 area: puntenregeling, clubstation+afdeling cell, E/F section headings, editor numbering). Privacy (R25) holds: the report exposes only callsign, section, afdeling, band count, QSO count, multipliers and score — a test asserts no email/address/operator name reaches the HTML. ## Tests **305** (was 296). New: section grouping and rank order, band/QSO/multiplier counts from the stored score basis (including an ineligible-band entry counted under `-`), contest-number pass-through, beker per-afdeling grouping with placeholder totals, year-so-far equals the yearly builder, privacy (no `@example`, no address), manager-only access, empty-month placeholders, unknown contest redirect, dashboard link. Verified live on the running stack; entry rows match the ingested corpus (PD1SCO 2 bands / 5 QSOs / 25).
Found while answering the owner's question about wrong-band checks: all three
parsers returned None on an unmappable band and edi/adif skipped the record, so a
QSO outside the contest bands vanished from the import — a 6cm/3cm EDI file (whose
PBand the parser never knew) arrived as an empty log with no warning. Silent loss on
import violates the never-auto-reject rule's own spirit: the manager cannot
adjudicate a contact nobody can see.

E9 fix (out-of-band):
- ParsedQSO.band and qso.band are nullable (migration 0008 drops NOT NULL).
- The QSO is kept everywhere: manager drill-in and summary sheet show it as band '-',
  scoring excludes it from QSO points and multipliers, cross-log pairing ignores it,
  and the entry carries ONE INELIGIBLE_BAND flag (a 6cm file would otherwise flood
  the report with identical rows).
- EDI export files no band file for it — EDI cannot declare a PBand, and inventing
  one would corrupt the export.
- The view layer mirrors the flag in its banner so preview/manager agree with what
  was stored.

E11 (modes):
- MODE_NOT_ALLOWED per offending QSO; never a rejection. USB/LSB normalise to SSB,
  a blank EDI mode column is not a violation (the standard allows leaving it blank).
- Bug found on the way: the EDI parser stored the numeric mode code verbatim, so
  every legitimate FM log would now read as mode "6" and trip the check. Codes
  1/2/6 translate to SSB/CW/FM at parse time (IARU R1 Vienna 1998 4.12.2); unknown
  codes stay verbatim and get flagged, which is correct because cross-mode (4) is
  not permitted by the reglement.

Corpus gains 16-FLAG-ineligible-band.cbr and 17-FLAG-disallowed-mode.cbr with README
outcomes. Tests: 296 (was 283).
Caught in live verification, not by the suite: SQLite models enums as plain text,
so the two new FlagType values passed every test, but the first real ingest with an
INELIGIBLE_BAND flag would die on Postgres enum input validation. Migration 0009
adds both values with ALTER TYPE ... ADD VALUE inside an autocommit block (the
ALTER cannot run in a transactional DDL step); downgrade raises, because enum
values cannot be removed and older builds would then refuse the column.
The Contest Manager publishes the month's outcome as a document of a fixed shape
(their current PDF 'Uitslag_NLC_249', examined in coordinate space, so the layout
below is the real one rather than a reading): title with editor number and Dutch
month name, per-section uitslag in their column order (Call | aantal banden | QSO |
Multipliers | Score | VRZA afd. | Afd. Punten) with the reglement's section
headings, the Afdelingsbeker with the month's entrants per afdeling, and the
year-so-far per section.

- app/documents/nlc_report.py builds the data; the year-so-far reuses
  build_yearly_standing_data so the participation/drop rule is never reimplemented.
- /manager/contest/{id}/nlc-report?nr=249 renders it print-ready (A4 stylesheet);
  the manager exports the PDF from the browser print dialog as they do today.
- The uitslag counts the score's basis: eligible-band QSOs, distinct band count,
  summed multipliers. An ineligible-band entry stays visible (counted under '-').

Everything the document cannot honestly produce is a placeholder, not a guess:
the editorial prose and closing (the manager's own voice - the robot does not write
manager copy) and the Afd. Punten / beker TOTAAL (their formula does not follow from
the sample; open question #9, with the clubstation+afdeling cell and the E/F
section headings).

docs/examples/nlc-report-demo.html is a rendered sample from the live stack.
Tests: 305 (was 296).
The Contest Manager's own explanation of the punten rule (VRZA news with NLC 236,
August 2025): one point as soon as a member makes a connection, another point for a
connection on an additional band, and every five QSOs on a band count for a point.
That is per band: ceil(logged QSOs on the band / 5). Verified against ALL 35
per-entrant Afd. Punten cells in the manager's published uitslagen 245-249; the
tie-breaker between ceil and floor-plus-base is NLC 247's PA2JCB (one band, 25
QSOs, 5 punten).

- app/scoring/afdelingsbeker.py: afdelings_punten(per-band logged QSO counts) and
  nlc_editor_number (226 = ?? no: anchor 236 = August 2025 per the VRZA news, one
  per consecutive month, confirmed 245..249) -> the report's nr is automatic and
  still overridable with ?nr=.
- The report now fills per-entrant Afd. Punten (blank for members without an
  afdeling) and the beker rows: the month's summed punten plus the year-so-far
  TOTAL. Section headings E/F follow the reglement's category wording.
- docs/open-questions-*.md: 9 answered with source; nuances left to the manager
  (duplicates toward punten, the inzenders-column carry-over) confined to one file.

Tests: 307, including the published-cells regression set.
Author
Owner

Follow-up on this PR (commit cc28b0b): the afdelingsbeker punten are now implemented — found the Contest Manager's own explanation of the rule in the VRZA news that accompanied NLC #236 (August 2025):

"Als je lid bent van een VRZA afdeling krijg je al een punt voor je afdeling zodra je één verbinding hebt gemaakt. Maak je dan ook op een andere band nog één verbinding dan is dat ook een punt. Verder tellen elke 5 QSO's op een band voor een punt."

That is per band: ⌈logged QSO's on the band / 5⌉. Verified against all 35 per-entrant Afd. Punten cells in the manager's uitslagen #245–#249; the case that pinned the reading is NLC 247's PA2JCB (one band, 25 QSO's, 5 punten — ceil, not floor-plus-base).

With it:

  • The uitslag's Afd. Punten column fills (members without an afdeling stay blank).
  • The beker rows carry the month's summed punten and the TOTAAL (year-so-far).
  • The editor nr is now automatic (anchor: 236 = August 2025, one per consecutive month — matches #245–#249 exactly); ?nr= still overrides.
  • Section headings E/F follow the reglement's wording (SWL's, Operators van de BES-eilanden).

Also confirmed from the current reglement text at vrza.nl: "UHF 70cm en hoger" is one band (Band.HIGHER correct), logs are delivered per band (the per-band upload model is exactly right), the SWL 3× rule, the 14-day window, the required multi-op roster and the medal thresholds (1 always; 2 at ≥5; 3 at ≥10 participants; certificates at ≥8).

Two nuances left as open questions (confined to one file): whether duplicate QSO's count toward punten (the robot counts logged rows), and the beker inzenders <month> column label vs its punten-sum content.

Tests: 307.

Follow-up on this PR (commit `cc28b0b`): **the afdelingsbeker punten are now implemented** — found the Contest Manager's own explanation of the rule in the VRZA news that accompanied NLC #236 (August 2025): > *"Als je lid bent van een VRZA afdeling krijg je al een punt voor je afdeling zodra je één verbinding hebt gemaakt. Maak je dan ook op een andere band nog één verbinding dan is dat ook een punt. Verder tellen elke 5 QSO's op een band voor een punt."* That is per band: **⌈logged QSO's on the band / 5⌉**. Verified against all **35** per-entrant `Afd. Punten` cells in the manager's uitslagen #245–#249; the case that pinned the reading is NLC 247's PA2JCB (one band, 25 QSO's, 5 punten — ceil, not floor-plus-base). With it: - The uitslag's `Afd. Punten` column fills (members without an afdeling stay blank). - The beker rows carry the month's summed punten and the **TOTAAL** (year-so-far). - The editor `nr` is now **automatic** (anchor: 236 = August 2025, one per consecutive month — matches #245–#249 exactly); `?nr=` still overrides. - Section headings E/F follow the reglement's wording (`SWL's`, `Operators van de BES-eilanden`). Also confirmed from the current reglement text at vrza.nl: "UHF 70cm **en hoger**" is one band (`Band.HIGHER` correct), logs are delivered **per band** (the per-band upload model is exactly right), the SWL 3× rule, the 14-day window, the required multi-op roster and the medal thresholds (1 always; 2 at ≥5; 3 at ≥10 participants; certificates at ≥8). Two nuances left as open questions (confined to one file): whether duplicate QSO's count toward punten (the robot counts logged rows), and the beker `inzenders <month>` column label vs its punten-sum content. Tests: 307.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
public/vrza-contestrobot!13
No description provided.