# Open Meet Display Integration Prompt

Paste the prompt below into your coding assistant. It points the assistant at the raw files of
version 1.0, pinned under `/omd/1.0/` so they never change, and needs nothing else.
[`/omd/llms.txt`](/omd/llms.txt) lists every raw file.

The assistant first inspects how your product draws a screen, proposes a plan, and stops for your
review. After you approve, it builds the receiver, tests it against the examples and the
conformance fixtures, and walks the spec's checklist. One check needs you: a send from 2Timer in
Chrome.

---

## Build a receiver

Use this if you make a scoreboard, LED or video board, broadcast or live-stream graphics, or an
announcer screen, and want meet programs to drive it.

```text
I want our product to receive Open Meet Display (OMD) 1.0. Meet-management software (the
producer) sends us one JSON message per HTTP POST that says what one display should show now: a
start list, results, team scores, an announcement, or a blank board. We (the receiver) show it.

Sources. Read the spec and the examples before planning. Treat these files as the authority over
anything you remember about scoreboard protocols. They are pinned to 1.0 and never change; cite
these URLs in code comments, not the unversioned ones.
- Spec: https://2timer.app/omd/1.0/spec.md
- Examples: https://2timer.app/omd/1.0/examples.md (links to one JSON file per message, and the
  probe reply)
- Message schema: https://2timer.app/omd/1.0/schemas/meet-display-v1.schema.json (JSON Schema
  2020-12)
- Probe reply schema: https://2timer.app/omd/1.0/schemas/probe-reply-v1.schema.json
- Conformance tests: https://2timer.app/omd/1.0/conformance.md (each test's check and pass
  condition)
- Fixtures: https://2timer.app/omd/1.0/fixtures/index.json (crafted test messages, each with the
  reply a conforming receiver gives)
- Reference receiver: https://2timer.app/omd/1.0/tools/omd-test-receiver.mjs (Node 18+, no
  dependencies; answers exactly as a correct receiver must)
- Checker: https://2timer.app/omd/1.0/tools/omd-check.mjs (Node 18+, no dependencies: node
  omd-check.mjs message.json, or --probe reply.json)
- Every raw file: https://2timer.app/omd/1.0/llms.txt

Step 1: inspect and report. Do not write code yet. Inspect our codebase and tell me:
1. How our product draws a screen today: the layouts, scenes, layers, or templates we have, and
   which of them could show a start list, results, team standings, or a text message.
2. How outside data reaches us now, if it does, and whether we already run an HTTP server. Name
   the language and framework.
3. How we name our displays or surfaces. OMD's display.target is a free-text name such as "main",
   "field", or "stream" that the operator types into the meet program, and we decide which names
   we understand.
4. How many rows each layout fits, and where long names or team names would have to be cut.

Step 2: propose, then stop. Propose which message kinds we support first and which layout shows
each, and how display.target names map to our surfaces. A receiver that shows only results is
complete, as long as it accepts the other kinds. Stop for my review before implementing.

Step 3: build, after I approve the plan:
- An HTTP listener whose address, port, and path the operator can change. Default to port 43339
  and path /omd. Let the operator choose between this computer only (127.0.0.1) and the local
  network (all interfaces), and tell them the firewall must allow the port for the second.
- OPTIONS on that path answers 204 with Access-Control-Allow-Origin, Access-Control-Allow-Methods:
  GET, POST, OPTIONS, Access-Control-Allow-Headers: Content-Type, Access-Control-Max-Age: 86400,
  and Vary: Origin whenever we echo the request's origin.
- GET on that path is the probe. Answer 200 with {"accepts": ["1.0"], "generator": {"id": "<a
  short name for our product, picked once and never changed>", "name": "<our product's name>",
  "version": "<our version>"}, "targets": [<the display.target names we understand>]}, or 503
  with Retry-After while we are running but cannot show anything yet. The producer uses it to
  decide what to send and to show the operator what it reached.
- POST on that path takes one message as the UTF-8 JSON body. Accept: 200 {"ok": true}. Refuse:
  400 for a body that is not JSON, and 422 for a message with no "omd" block or an omd.version
  whose major we do not read, each with {"accepts": ["1.0"], "errors": [{"path": "<JSON
  pointer>", "message": "<short reason the operator can act on>"}]}. Every response, success or
  error, carries Access-Control-Allow-Origin, or the browser hides it and the operator sees a
  failed send.
- An allow-list of producer origins the operator can edit, starting with
  https://manager.2timer.app. Echo the request's Origin when it is on the list. Use * only behind
  a setting clearly marked for testing.
- A log the operator can see: each message's time, display.target, omd.kind, omd.generator.name,
  and omd.sent_at, and every refusal with its reason.
- Rendering for the kinds we agreed, on the surface each target maps to.

Step 4: follow these rules while building. Each is easy to get wrong.
- Reply as soon as the message is parsed and accepted, and render afterward. The producer gives up
  after 5 seconds and tells the operator the send failed.
- Every message is a complete snapshot for its display.target. Replace what that target shows;
  never merge rows, never append. A row that is missing from the new message leaves the board.
  Receiving the same message twice renders it again.
- An omd.kind or display.mode we do not support is accepted with 200 and changes nothing on the
  board. Never refuse it: producers send start lists, team scores, and announcements to every board.
- Refuse nothing beyond the 400 and 422 cases in Step 3, with one choice of ours: an unknown
  display.target is either shown on a default surface or refused with an error naming the target.
  A later 1.x minor only adds, so read every omd.version that starts with "1.".
- Ignore fields we do not recognize, at any level. A field with no value is left out of the
  message; treat a missing field and a null one the same, because older producers may send null.
- A kind's content sits under its own name: read message[message.omd.kind]. Board lines are always
  that block's rows: start_list.rows, results.rows, team_scores.rows. event, when present, sits
  beside the block at the top level.
- Every value is display-ready text. Show place, mark, seed, wind, points, and names exactly as
  sent; never parse a result, never compute a place, never reformat a name. Pick between
  team.name and team.short_name by the width we have.
- Render rows in the order received. The producer sorts and pages; we never page. When an event has
  more rows than fit, the producer sends the next page later as a new message.
- Results carry only athletes with an outcome, and a finisher only once placed; a runner still on
  the course is not in the message. A placed row has no status. For a non-finisher, place already
  holds what a results sheet prints (DNF, DQ, DNS, FS, SCR, and NT, NM, or NH for status nr), so
  the place column prints as-is. Use status (dnf, dns, dq, fs, scr, nr, lowercase) only for
  styling, and show an unfamiliar status code as text, never an error. Only a dq among
  non-finishers carries a result.
- In cross country results, points is the points a runner's position carries, and scoring_role
  says whose they are: "scorer" counts toward the runner's team, "displacer" holds a position
  after the team's scorers and only pushes other teams back (a printed team list shows it in
  parentheses), and a row without it scores nothing. Never add up rows for a team score: a team
  short of runners can be scored with ghost runners, which are not rows, and a page shows only
  some rows. The team score comes from team_scores.
- A start list has no row for an empty lane. start_assignment is missing when no lane is assigned.
- relay_legs, when present, lists the leg runners in running order; participant_name is then the
  relay team name.
- team_scores.scope "meet" is standings summed across a division's events. "event" is one race's
  standings and comes with that race's event block: title the board from event.name and match
  event.id to the results we showed. division_name is the division's name, missing when there is
  none. Show every team or truncate on our side; team scores are never paged. In cross country
  low scores win, and the rows still arrive in place order.
- clear blanks that target to our idle or logo state.
- announcement.message is plain text. Wrap it to fit, and never interpret any string in a message
  as HTML or markup: these messages can reach a live board or a broadcast. Stop showing an
  announcement after its expires_at when one is set.
- The race clock is not in OMD. If we show a running clock, it keeps coming from the timing system
  on its own channel.
- display.connection_id identifies the sender; it is not a credential. entry_id lets us match a row
  between snapshots, for example to animate a new finisher; do not store it beyond the meet.
- Messages may arrive out of order. If that matters for a surface, drop a message whose
  omd.sent_at is older than the one already showing on that target.

Step 5: add automated tests that:
- POST every example message to our listener and check each gets 200 and renders on its surface.
- POST every fixture in fixtures/index.json and check the reply matches its "expect": accept is
  200 or 204 and shown; ignore is 200 or 204 with the board unchanged; refuse-400 is 400;
  refuse-422 is 422 with accepts; accept-or-422 is whichever we chose for an unknown target. The
  fixtures include a made-up kind ("schedule"), a body that is not JSON, omd.version "2.0", and a
  message with no omd block. Every reply carries the CORS header.
- GET the probe and check the reply validates against the probe reply schema (or passes
  omd-check.mjs --probe).
- Send OPTIONS and check it answers 204 with every header in Step 3.
- Validate each example against its schema with a JSON Schema 2020-12 validator (for example
  Ajv's 2020 build), so the test data is known to be the real shape.

Step 6: compare with the reference receiver. Run the curl commands in the spec's "Testing and
conformance" section against our listener and against the reference receiver, and show me any
reply that differs.

Step 7: report. Walk the spec's checklist (the "Checklist" section), item by item, and show me
which pass. One item is a send from a browser-based producer, which you cannot do: tell me to
press Test connection in 2Timer (it should name our product and say it reads 1.0), send a start
list and results from 2Timer in Chrome, allow Chrome's local-network prompt when it appears, and
report the browser, its version, and what happened.
```

This prompt is dedicated to the public domain under
[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/).
