PRODUCT UPDATES

What changed, release by release.

Every product in the Touchline360 suite ships from one place and shares one changelog. Entries say which product they affect.

0.20.0

Added

  • The capture can run the Eye. Put a clip in marketing/footage/ (or point CAPTURE_PULSE_FOOTAGE at one) and capture:pulse starts the real vision runner over it, waits for its first annotated frame, and composes the Kickoff Pulse hero on that frame. It goes through vision_runner.start() by way of the new apps/pulse/scripts/start_eye.py, which is the call the Match Console's own button makes, so what runs is what a coach's click runs. The detector defaults to a stock yolo11n.pt: vision/config.py already maps COCO's person and sports ball onto the canonical classes, so the club's trained weights are an accuracy upgrade rather than a prerequisite. Verified end to end, detection, tracking and annotation, on a throwaway clip. Published stills still need footage the club owns, which is why marketing/footage/ is git-ignored: a still carries the frames it came from.

Fixed

  • The Eye wrote its frames where nothing was looking. build_argv passed --record from RECORD_DIR but left --snapshot, --stats, --data-file, the status file, the pid file and the pause flag on their own relative defaults. The runner is spawned with cwd=REPO_ROOT while the app resolves its files from *its* working directory, and both honour KICKOFF_* overrides, so the two agreed only when the app ran from apps/pulse with nothing set. Anywhere else the console showed "No frame yet" for a run that was working perfectly, and its events never arrived. Every path the runner writes is now passed explicitly.

0.19.1

Changed

  • No em dash in Kickoff Pulse either. The house style now covers the app, not just the site: 545 of them across 97 files, in coach-facing copy, docstrings and comments alike, each rewritten to a colon, a comma or two sentences rather than swapped for a hyphen. apps/pulse/tests/test_no_em_dash.py holds the line, and names the two files that keep theirs on purpose: report.py, whose _PDF_REPLACE maps the character to a hyphen and so has to contain one, and tests/test_report.py, which feeds them in to prove that mapping works.
  • Streamlit's empty-value dash is a hyphen. Roughly two dozen places printed a bare em dash to mean "nothing recorded".

Fixed

  • `aiConfigured` told production its AI was off. The field was bool(OPENAI_API_KEY), and this deployment runs a self-hosted Ollama, so the one line an operator glances at said the feature was unconfigured while it was working. It now asks whether the *configured* provider has what it needs, and checks.llm reports the provider, the model, whether the server answers, and for Ollama whether that model is actually pulled. A running Ollama with the model missing used to fail only at the moment a coach asked it something.
  • The shared-Postgres tests raced the container they start. The official Postgres image runs a temporary server during initdb that listens on the Unix socket only, so pg_isready reported ready against a server about to be replaced, and the next command got "the database system is shutting down". The probe now goes over TCP, which the init-phase server never listens on. The whole Pulse suite passes: 621 tests, no errors.

0.19.0

Added

  • Kickoff Pulse's own screens are captured now. pnpm --dir apps/web capture:pulse seeds a sandboxed match in its 68th minute, starts Pulse headless, drives it with Playwright and writes hero and capture at 2880x1800 like every other still. docs/MARKETING-ASSETS.md had said for months that Pulse was a desktop app and therefore unreachable; that was only ever true of the runner that drives next dev. Pulse is a Streamlit app, so it serves over HTTP. It also boots without whisper, PyAudio or the YOLO stack, because none of them are imported by the UI layer. Kickoff Pulse no longer falls back to a schematic anywhere on the site.
  • Interceptions, clearances, crosses and dribbles are real event types. Manual Entry offers them and the voice tracker logs them, but icons.py had no category for any of them, so each one drew a grey dot labelled "Event" in the live feed and the timeline. All four now have a colour, a glyph in both renderers, and their own name.

Changed

  • Kickoff Pulse's app really does follow the suite now. The 0.18.0 entry below claimed this and was wrong: the design tokens were changed but almost nothing consumed them. The background was a hard-coded navy-to-blue gradient, .streamlit/config.toml still pinned Streamlit's own theme to light with Pulse Blue as the primary colour, which drives every native widget our CSS never touches, and a scatter of rules carried literal blues. Running the app and looking at it is what found this. The ground is now the suite's flat #0B0D0E, panels are solid surfaces with a visible border instead of glass over a gradient, the primary button is the suite's lime, and the page titles are plain white.
  • Exported artwork names the teams. The share card and the timeline image both printed "HOME 2 - 1 AWAY" while naming the clubs in the line above. The share card is the most public thing the product makes, and a scoreline in a group chat should say who played.
  • Streamlit's "Deploy" button is gone. Pulse runs on a coach's laptop; it offers them something they can neither do nor want.

Fixed

  • The Timeline page no longer crashes on two events in the same second. Its buttons were keyed on the event timestamp, so a duplicate raised StreamlitDuplicateElementKey and took the page down with a traceback. The Eye emits several events a second, so this was reachable in a real match. The row index is now always part of the widget key; the timestamp stays the identity used to update an event.
  • A substitution counts as one. The sample match logged "sub", the display key, where every writer in the app stores "substitution", so the feed showed a substitution while the Subs stat read 0.
  • No em dash in the images the site serves. The timeline separated an event's summary from its detail with an em dash, which put one on touchline360.com in pixels rather than text.

0.18.0

Changed

  • No em dash anywhere on the public site. A house style, so it is a test: components/site/noEmDash.test.ts scans every source file under the site and fails on one, comments included. Each of the forty-one was rewritten rather than swapped for a hyphen, because a dash usually marks a sentence that wants a colon or a full stop.
  • The pricing page asks once. Three cards each carried their own "Get a free quote" button; they all led to the same conversation, so there is now one button underneath the three.
  • Who we work with is said plainly. The pricing hero and the plan note both name professional clubs, semi-professional and amateur clubs, and grassroots clubs, because "a club" covers very different conversations.
  • Kickoff Pulse's app follows the suite. The Streamlit theme drew in Pulse's own navy and blue; its design tokens are now Touchline360's, so the app a coach operates matches the images it exports and the rest of the suite.

Fixed

  • The Debrief product page started higher than its siblings. Its container had no top padding while every other product page gets clamp(40px,6vw,80px) from the shared stylesheet. All eight now place the logo at the same height; padding only, so the full-bleed bands below still run edge to edge.
  • Two Kickoff Pulse tests only passed from one directory. They loaded scripts/rig_capture.py by a path resolved against the working directory, so they passed from apps/pulse and failed from the repository root, where the suite is normally run. The script was there all along. Resolved relative to the test file, and the whole Pulse suite now passes from anywhere.

0.17.0

Changed

  • Kickoff Pulse exports in the suite's colours. The three images Pulse renders from a match — the timeline, the momentum chart and the share card — drew on a light ground and a navy card in Pulse's own blue and red, so they read as another product's artefact beside anything else in the suite. They now use Touchline360's ground, text and line colours, and the team colours come from the suite's own diagrams so a Pulse export and a Debrief diagram agree on which side is which (EXPORT_* in apps/pulse/brand.py). This is a product change, not a marketing one, and was made deliberately: the share card is the most public thing the suite produces — a coach puts it in the staff group chat — so it should look like the suite wherever it lands. The app's own chrome is untouched; only what a coach exports changed.
  • Two event colours that only worked on white. corner and offside were a dark slate chosen for a light background and were almost invisible once the timeline moved to the suite's near-black. Lifted to a slate that reads on it.

0.16.0

Added

  • Kickoff Pulse has real images. It is a desktop app, so the capture pipeline cannot drive it — but apps/pulse/render_assets.py renders the match timeline and the post-match share card from a real event log, which is a truer thing to show than a screenshot of a screen. Both are published, in Pulse's own palette because that is what the product looks like, and the two captions now describe what they actually are. Its third slot is a screen and stays a schematic.
  • Every logo is cut from a source in the repo. brand/source/ now holds the supplied artwork for eight products and brand/cut_logo.py regenerates the mark and lockup for any of them. Select XI is the exception and brand/README.md says why: what was supplied for it is a brand-guide sheet, not a single logo.

Changed

  • The shipped logos and the documented script now agree. The committed assets came from an earlier variant of the cutter and were a little looser, so re-cutting produced a different crop every time — which would have made the new archive misleading rather than useful. All seven re-cut, all registry sizes updated, all checked at equal height on one contact sheet.

Fixed

  • The footer declared the suite lockup as 900×780, against a file that has never been that tall. next/image reserved the wrong box for it; the CSS sets width with height:auto, so the logo drew correctly and only the reserved space was wrong, which is why it went unnoticed.

0.15.0

Added

  • PlayerPath shows its own screen. Like ScoutLens before it, the page claims the squad list and the path run in the app today, and now carries a screenshot of exactly that. The frame includes the product stating its own limit — minutes come from lineups, so a start counts as ninety and a substitute appearance counts as none — which is the claim the page makes in prose two paragraphs below. Both announced products now back their claims with the screen rather than asking to be believed.
  • The brand source and its cutting script live in the repo. brand/source/ holds the supplied artwork and brand/cut_logo.py turns any of it into the mark and lockup; re-cutting no longer depends on a file on somebody's desktop. The script reproduces the committed Blueprint assets byte for byte.
  • A font dropped into `apps/api/app/fonts` becomes a PDF fallback. DejaVu has no CJK glyphs and a CJK face is roughly 16 MB a weight, which is not worth carrying in the image for an English-only interface. So the hook ships instead of the font: any TTF placed in that directory is registered as a fallback for characters the main face lacks, with no code change. apps/api/app/fonts/README.md says so, including the licence requirement.

0.14.0

Added

  • Blueprint has its logo. The supplied artwork is cut into the same two assets every other product carries: a 320×320 mark and a 900-wide lockup, both on transparency, with the registry declaring the real pixel size so the dimension test can hold it. Blueprint's hero shows the lockup instead of a typeset wordmark, which makes all eight product pages uniform in kind as well as in layout. Both layers now have a mark, so the landing's layers band carries them the way the product chips do.

0.13.0

Added

  • ScoutLens shows its own screen. Its page claims the opponent card "runs inside the app today behind a switch"; it now carries a screenshot of exactly that, which is the claim's evidence rather than an assertion. The frame shows all three epistemic states at once: *observed* recent form with the match it came from, *derived* attacking threat with the metrics behind it, and *unavailable* set pieces where the evidence does not reach. The capture run sets TOUCHLINE_RESEARCH_SURFACE=1 for itself; nothing about what a deployment serves changes.

Removed

  • The `/og/<product>` capture shot. It had been reporting a skip on every run because the route never existed. It was a stand-in for Open Graph cards, which Next's opengraph-image convention now generates per product page and serves in production — so the right fix was to delete the placeholder, not to build a second way of doing the same job.

0.12.0

Added

  • A player's name is spelled properly on paper. The PDFs now embed DejaVu Sans (vendored under the Bitstream Vera licence, notice included), so Šimić, Özil and Łukasz print as themselves instead of being folded to Simic, Ozil and Lukasz. This is about squad data, not localisation: the interface is in English, and an embedded font does not make the product Greek — but any English-speaking club in Europe has players whose names the core PDF fonts cannot spell, and a development report goes home with the player. The face covers Latin, Latin Extended, Cyrillic and Greek; it has no CJK glyphs, so Chinese, Japanese and Korean text still degrades. Subsetting keeps a rendered board at roughly 23 KB. A deployment missing the files falls back to the previous transliteration rather than failing to produce a document.
  • The opponent has a real feed too. Riverside United's side of the demo match is now a different shape rather than a smaller copy: plenty of evidence for how they break and how they press, and thin evidence for how they build. That is what an opponent report actually looks like, and it keeps both engine behaviours visible — an established pattern where the evidence reaches, a small sample where it does not.

Changed

  • Marketing stills recaptured against the new product-page heroes and the fuller feed.
  • PDF assertions read the document instead of its bytes. Embedding a TrueType face stores text as glyph indices, so searching the raw PDF for b"STEP 1 OF 2" stopped working — which is a better prompt than it looks, because extracting the text is what those tests always meant. pypdf is pinned as a test dependency and tests/pdf_text.py reads the words back.

Fixed

  • Health reports whether the embedded PDF face shipped. A missing font degrades silently — documents still render, with player names folded to ASCII — so the only way to notice was a coach reading a report with their player misspelled. checks.pdf_fonts now says.
  • Two PDF renderers crashed with a `NameError` while the font was being wired in: module-level helpers take a pdf handle, not self, and the family lookup had been written as though they were methods. Caught by the suite before it left the branch.

0.11.0

Changed

  • Every product page opens the same way. Debrief's hero was a two-column grid with the logo top-left while the other seven were centred headers, and the lockups were sized by width — so Select XI's near-square logo stood half again as tall as ScoutLens's wide one. One ProductHero now serves all eight: the lockup is sized by *height* so each occupies the same band, then the eyebrow, headline, lead and actions, centred. Debrief keeps its screenshot, now full width under the hero. Blueprint, which has no lockup yet, sets its name in the same band so the page keeps the rhythm. The registry declares each lockup's real pixel size (they run 1.14:1 to 1.97:1) and a test reads the PNG headers so a declared size cannot drift from the file.
  • The product tabs carry their marks. The chips on the landing were a row of words; each now shows the product's logo beside its name, which is what a coach recognises first.
  • The Debrief page shows real screenshots. Three of its media slots pointed at ids the capture pipeline never wrote, so they drew schematics; they now point at the captures that exist.
  • A PDF in a language Latin-1 cannot spell is transliterated, not blanked. "Model gry mlodziezowej" used to print with question marks where its diacritics were; it now reads, and accents Latin-1 does have are untouched. Cyrillic, Greek, CJK and emoji still degrade, and that limit is documented in the code: fixing it means embedding a Unicode font, which is a size and licensing decision.
  • The demo match's feed carries more evidence. All five game-model principles now clear the sample threshold instead of two; the opponent's thinner side of the same feed still comes back as small samples, so the engine's restraint stays under test.

Fixed

  • A share link no longer carries the squad name. The public board payload dropped created_by and its source ids but kept team_id, so anyone holding the URL learned which team drew the board. A player or parent needs the drill, not the club's squad name.
  • The whiteboard warns before the tab closes while a board has unsaved work or a save has failed or conflicted. Autosave is 1.2s behind at best and a failed save can leave work pending indefinitely.
  • `migrate.py`'s identity bootstrap no longer takes a deploy down. It caught only SystemExit, so an import error or a bad TOUCHLINE_BOOTSTRAP_* value killed the pre-deploy *after* every migration had committed — the worst place to stop. It now matches the sample-import block below it and continues.

0.10.0

Fixed

  • The version check now lives inside the UPDATE. Every conditional save — sessions, whiteboards, curricula, units and the bench log — read the row, compared the version, then wrote. Two coaches could both read version N, both pass the check, and both write N+1, and the later write silently destroyed the earlier one's work with no 409 anywhere. training_store.update takes an if_version and puts the comparison in the statement that does the write (AND t.version = %s), so the database decides. A lost race is a 409 with the version that actually won. Verified against a real Postgres 16, not only in memory mode: two writers who both read version 1 end with the first coach's board intact and the second refused.
  • The film session was unreadable. Its teaching points sat in a grid whose actions column was auto, so six buttons claimed their width and squeezed the text into a column one word wide; the actions now take their own row. The heading was the raw external match id — a storage key, not a title — and is now the team, with the id still on the line below for anyone who needs it.

Added

  • The demo match carries a synthetic event feed. It had none, so the review screen correctly reported that it had no tactical findings — true, and it meant nobody could see what the review does. training_demo_match.py builds Touchline FC 1–1 Riverside United to match the story the demo already tells: sixteen shots, four on target, three crosses where the far-post run arrived late, and an equaliser from our own corner. The events are invented and say so (evidence.synthetic, the SYNTHETIC badge, "Demo (synthetic)"); every finding over them is computed by the real engine against the real game model, and the two that lack evidence stay labelled a small sample rather than being promoted. Nothing here is a hand-written finding.
  • Three more marketing slots show the product. With the feed and the layout fixed, debrief/match-review, debrief/findings and debrief/film-session are captured from the running app. Their captions were rewritten to describe what the screens actually show — the middle one is the coverage the findings rest on, not a list of findings, so it is now titled "What the evidence supports".

0.9.0

Added

  • Blueprint can be written, not just read. A curriculum grid cell is now a button: an empty one opens a new unit for that age group and phase, a unit opens for editing. A unit says what is taught, which game-model principles it teaches (chosen from the team's model, never typed, so coverage can count them), its objectives, how you would know it landed, and notes for the staff. Saves are conditional on the version loaded and deletes take the cell back to being an invitation. The API had all of this; the screen had none of it, so a club's curriculum had to be loaded for them.
  • Marketing material, captured from the running product. Six slots now show the real thing: the board library, the editor with a coach placing players, a routine playing through, the board in one frame, the printable session with its epistemic labels, and Debrief's match-week board. The pipeline learned to address the demo seed's own ids (${demo.sessionId} and friends), and a shot id is now unique per product rather than globally, because every product has a hero.
  • Whiteboard: the board goes to paper. GET /api/training/whiteboards/{id}/pdf renders a board one page per step and has been tested since it was written, but no screen offered it: a coach could print the session and not the drill. The board header now has "Print (PDF)", which flushes any pending autosave first.

Fixed

  • A slow save turned into a phantom conflict and could discard the coach's work. The whiteboard's autosave effect re-ran on saveState, so 1.2s into a save it scheduled a second PUT carrying the same baseline version. The server answered 409, the board reported "Someone else saved this board first" when nobody had, autosave stopped for good, and the only control offered reloaded the server copy over everything typed since. The same gap made Print, Share and Add-to-session fail moments after an edit. SessionWorkspace had guarded against this all along; the whiteboard now does too, and an e2e drives a 2.5s save and asserts no save reuses the baseline of the one before.
  • A curriculum title outside Latin-1 made the club document a 500. The filename went into a Content-Disposition header unsanitised, so any diacritic or quote raised UnicodeEncodeError. Same sanitiser and same try/except the other three PDF routes already used.
  • One stored number could pin a worker for minutes. A ladder's rung count was derived from an unbounded width, so a single diagram object could turn one PDF request into ~27 minutes of CPU and gigabytes of memory. Pitch geometry is now bounded at the model (which also excludes NaN and infinity) and the rung loop is capped.
  • Ctrl+Z in a step's title or notes undid the diagram. The typing guard sat *after* the undo/redo branches, so correcting a typo swallowed the browser's own text undo and silently reverted an unrelated diagram change.
  • Three client limits that the server rejects. The diagram title had no maxLength against a 200-character column, "+ Add step" was never disabled against a 40-frame cap, and auto-numbering could reach 100 against a 0-99 bound. Each produced a 422 the UI showed as a permanent, unexplained "Save failed". A new step is also named for where it lands rather than for how many there are.
  • Every row on the match-week board ran its two lines together — "Finishing Under PressureFinishing". .rowMain was referenced by four rows and defined in no stylesheet, so the detail <small> stayed inline.
  • The landing stopped asking the browser which stills exist. It fetched every product still on load and caught the 404s, costing seven failed requests a visit and filling the console. The server answers from the filesystem in one pass now, and a slot with no capture renders its schematic directly.

Changed

  • PlayerPath's epistemic union gained the two labels the API can actually emit (coach_interpretation, ai_suggestion), the ScoutLens nav item says it is in development and off unless the deployment enables it, the inverted 024/025 comment in 026_curricula.sql is corrected, the dead lib/eyeCapture.ts is removed, and /products/kickoff-pulse/download joins the certification script's public pages.
  • Blueprint's page says what the screen can now do, and the test-count figure is 862 measured on the 11 September run.

0.8.0

Added

  • Debrief: empty states, the match-week board and the presenter. Home is now the match-week board (previous match findings, this week's sessions, next opponent, what needs a decision) with one action per row. Every workspace under Debrief opens usefully with no data and offers "Load the demo (synthetic data)", which seeds the demo match week and labels every record it creates. The film session presenter gains full screen (F), Home/End, an always-visible evidence badge and a reduced-motion-safe slide transition.
  • Whiteboard: an interactive coaching board. Coaches now draw on a board the way they would on a real whiteboard: a palette of shapes, equipment and players, a pen, dashed and curved passes and runs, colours, corner resizing, zoom and pan. A board is a sequence of steps, so a set piece or a routine plays through with players gliding between steps. Boards live at /training/whiteboard (API /api/training/whiteboards, migration 023), save conditionally on their version, and lead somewhere: a step becomes a library exercise or a block on a session, a block can take its diagram from any board step or open on a new board, and any step exports as a PNG. The diagram model grew (six new object types, colour, dashed, smooth, multi-point paths) without breaking a single stored diagram; the session PDF draws the new types. The Whiteboard product's "Open" now lands on the board, with the planner one click away.
  • Whiteboard: rotation, templates and palette keys. A knob above a selected goal, mini goal, ladder or rectangle turns it (Shift snaps to 15°), with a numeric rotation in the side panel; the SVG and the session PDF draw the turned outline. Ten built-in template boards (short and far-post corners, an attacking throw-in, rondo 4v2, positional 6v4, the build-up pattern, a pressing trap, the counter-press, a wide overload and a penalty routine) sit in a "Start from a template" gallery on the boards page; each has two to four steps with the coaching note for the step, and using one copies it into the coach's boards (GET /api/training/whiteboards/templates, POST /api/training/whiteboards/from-template/{key}). The palette answers single keys (V, P, A, R, C, T, 1, 2, 3), listed in the hint, and grows to finger-sized targets on a touch screen.
  • Whiteboard: the board goes to paper. GET /api/training/whiteboards/{id}/pdf renders a board one page per step and has been tested since it was written, but no screen offered it: a coach could print the session and not the drill. The board header now has "Print (PDF)", which flushes any pending autosave first — the PDF renders from the stored board, so an unsaved step would otherwise have gone to the training ground as it was rather than as it is.
  • ScoutLens, PlayerPath, PitchView and Blueprint have a screen. Four products existed only as an API. Each now has a workspace a coach reaches from the app: /scoutlens (the opponent card and the profile behind it), /squad and /squad/[playerId] (the squad list, a player's season line and the printable path report), /data (the source-coverage console, reading the real registry and the real licence policy) and /blueprint (the club's game model, principles and season priorities). /players belongs to the stashed research surface, so PlayerPath's list is /squad.
  • Matchday: the live bench log. /matchday/[matchId] keeps the running entry feed a coach types on the bench, ordered newest-first with the match clock, and hands it over to the review. This is Third Runner, shipped inside Debrief rather than as a separate product.
  • Every public page has a share card. A link pasted into a message now unfurls as a 1200×630 card drawn from the product registry: the product's name, the line the registry already carries, and how it stands — a shared ScoutLens link says "In development" on the card itself, so the honesty on the page survives being forwarded. One opengraph-image at the site segment covers every page; each product overrides it with its own. The card is drawn in type rather than composited from the brand PNGs, because public/ is not reliably traced into a standalone build.
  • `metadataBase` is set, so those cards resolve to a real origin. Without it Next resolves a generated opengraph-image to http://localhost:3000 and only warns, which would have shipped a card no one outside the build machine could fetch. siteOrigin() defaults to https://touchline360.com and a preview deployment overrides it with NEXT_PUBLIC_SITE_URL.
  • Four public pages, one per announced product. /products/scoutlens, /products/playerpath, /products/blueprint and /products/pitchview say what each one does and, under "Where it actually is", where it honestly stands today — no badge, no waiting list, no figure. The certification script now requires all four, plus /pricing, to serve 200 in production.
  • Public site pages: a footer on every public page; /privacy, /security, /docs and /updates (rendered from this changelog at build time — apps/web/Dockerfile now copies CHANGELOG.md, and railway/web.json watches it).
  • Kickoff Pulse product page with the setup guide; the NEXT_PUBLIC_KICKOFF_PULSE_URL variable is no longer read.

Changed

  • Pricing sells a quote, not a price list. /pricing carries no figure and no tier. It leads with a free quote and a free consultation, names the three shapes a plan takes — one tool, a few combined, or every team at the club — and explains why there is no price list: a number without a club's context turns away the club we have not met yet. components/site/Pricing.tsx holds those three routes instead of the launch tiers, and the e2e test asserts that no figure, no per-month wording and no checkout appears on the page.
  • The footer names Trequa and links to it. COMPANY_BRAND gains the parent site (https://trequa.io); the footer's Company column and its legal line both link out to it.
  • Blueprint and PitchView are layers, not a fifth and sixth tool. The product registry gains a kind ("tool" | "layer"), and the landing shows the two layers in their own band under the products: one is what the club intends, the other is what actually happened and what it can stand on, and every tool reads both. Neither is sold on its own. Groundwork stays in the registry as the engineering name for the same platform PitchView presents.
  • The site no longer claims more than the code does. A branch review found four places where the public pages overstated the product, each now corrected against what actually runs: - The landing said *"Every screen below is the running product."* Only one of the sixteen referenced stills has been captured; the rest fall back to a schematic drawn in the product's own diagram model. The copy says that, and ProductShot now marks the fallback (schematic) in its accessible label, the way the Debrief page's slots already did — a screen reader was being told a drawing was a screenshot. - The Debrief page captioned its reel *"Sixty seconds, end to end"* with no video in the repository at all. The caption follows the sources: it promises the walkthrough only when there is one to play. - The Blueprint page said the curriculum "runs inside the app today". Its API does; the screen can read and start a curriculum but not yet write a unit on one, and the page now says so. - The PitchView page said the provenance badge was "in use on some screens". It has no callers at all. The page now says it is built and not yet placed.
  • The test-count figure is measured, guarded and smaller. It read 1,448, sourced to counts that no longer matched any suite. It is now 857 — 459 on the API and 398 in the web app, both run on the day stated. Kickoff Pulse's 606 is no longer added in: it ships from its own app and nothing on this branch can re-run it, so it was a number that would go stale unnoticed. A test now asserts the headline equals the sum of the parts its own note names, and that the note carries a date.
  • The ScoutLens nav item stops promising a product the deployment may not serve. Its router only mounts under TOUCHLINE_RESEARCH_SURFACE, which a default deployment does not set; the menu description now says it is in development and off unless enabled.
  • The announced products show a date, not a badge. The landing's development rows link to the product's own page ("What ScoutLens does, and where it is") rather than to a contact form, and their evidence lines say what is built and what is left — for ScoutLens, the competition engines still being ported; for PlayerPath, stored profiles that pin a player across spellings and seasons.
  • Touchline360: the site sells the suite. The landing, the header and every product page lead with the products and a way to talk to us; sign-in is there for clubs already set up, not the first thing a visitor sees. The Kickoff Pulse page no longer points at a source checkout, a launcher or Ollama: it says what a coach needs at the touchline and that we set it up with them. The Select XI page carries Select XI's own copy (its marketing/WEB_COPY.md and App Store listing) instead of a paraphrase.
  • Whiteboard is a listed product. The training planner was always inside Debrief at /training (docs/TRAINING-PLANNER.md); the site now shows it as the fourth tool under its product name, Whiteboard, with its own page at /products/whiteboard and a place in the match-week loop, and says plainly that it is included with Debrief. The in-app route and nav item are still "Training".
  • Touchline360 has its logo. The suite mark is now the supplied circle-and-pitch (apps/web/public/brand/touchline360-mark.png, with the full lockup and its tagline "See the game differently" in the footer and app/icon.png as the favicon); the hand-drawn SVG mark is gone. The suite tagline in lib/productBrand.ts follows the logo. The app shell's rail shows Debrief's own mark.
  • Every product has its logo, and two more are announced. The suite's marks and lockups live in apps/web/public/brand/ and are referenced from the product registry in lib/navigation.ts (art). Product cards use the mark; product pages open with the lockup. ScoutLens (the customer-facing name for the competition intelligence being ported from ElevenIQ) and PlayerPath are announced on the landing; both gained a page and an in-app workspace later in this release, so the landing rows now carry a date and a link to the page instead of a "coming soon" badge.
  • Production certification matches the stashed research surface. scripts/production_certification.py only checks the competition, team, player, compare, questions and data-health routes with --research-surface; by default it certifies that they stay 404. The private tenant read is certified against the curated demo match when no --match-id is given.
  • Touchline360: the site is the suite, the app is behind sign-in. apps/web/app is now three route groups: (site) for the public Touchline360 pages, (debrief) for the signed-in app, (research) for the stashed research surface (docs/PRODUCT-INFORMATION-ARCHITECTURE.md). The landing at / is the Touchline360 suite, centered, listing only the built products — Debrief, Kickoff Pulse, Select XI — each with a page under /products/. Groundwork, ElevenIQ, Third Runner and Methodology are unlisted (listed: false in lib/navigation.ts) everywhere public and in the shell's Suite group. The "no material insight without evidence" card is gone.
  • Sign-in is its own page. /sign-in (with next=) replaces the sign-in form on /access, which is now the account page. Every route in (debrief) redirects to /sign-in without a valid session; /training/shared/* stays public. The local development bypass still counts as a session.
  • Brand mark. The committed PNG logo files were not valid images; the mark is now an inline SVG (components/BrandMark.tsx, app/icon.svg, public/brand/touchline360-mark.svg) drawn from the brand guide, and TouchlineLogo renders either the Touchline360 or the Debrief lockup. Kickoff Pulse's logo ships at public/brand/kickoff-pulse.png.

Fixed

  • The bench log was unreachable. matchdayApi.find returned the route's {"log": …} envelope instead of the log. {log: null} is truthy, so every if (!log) guard missed and the next property read threw: Third Runner crashed on open for every match. The client unwraps the envelope and a test asserts an empty result is null.
  • "Send to Debrief" wrote nothing and locked the log. The workspace called handoff(log.id) with no entry ids, and the API happily completed an empty handoff: no observation on the match, handed_off_at set, and the log read-only forever with no way back. The workspace now sends every entry, the API refuses an empty selection with 422, and a retry after a partial failure recognises entries it already sent (by the source stamped on the observation) instead of writing them twice.
  • A concurrent `POST /api/matchday/logs` returned 500. Two phones opening the same log raced past the pre-check into migration 024's unique index. The loser now gets the same 409 as the pre-check, and the log the winner made.
  • ScoutLens called a match "Most recent" that it could not know was recent. MatchIntelligence carries no kick-off date, so the opponent card was sorting by match-id string and labelling the last element as the latest result. The claim is gone and the limitation now says these matches have no date and are in no particular order — the sort remains, for determinism only.
  • Cross-tenant tests for the two new org-scoped surfaces. PlayerPath and the bench log were the only new APIs missing from test_isolation_matrix.py; both are now covered (profile, path, report, shares, objectives, roster; log header, entries, handoff and the finder), and player_path_store joins the fixture's Postgres monkeypatch list.
  • Two undefined references in the whiteboard work: a scrubber that assigned to an undeclared scrubbing ref in the shared board player (nothing read it; setPlaying(false) already did the job), and a missing shareUrl helper in the whiteboard workspace, which now builds the link from the API's path and falls back to the board's own shared route.
  • Four GroundworkFigure<null> inference failures in the Groundwork API test, which broke tsc for the whole web app.

0.7.0

Changed

  • Debrief: back to first principles. The product is three things for a grassroots or lower-league club: analyze your own matches, plan training and lineups, work the problem with AI. The competition, team and player intelligence built on licensed league data is stashed, not deleted: its routes moved under apps/web/app/(research) and 404 (via apps/web/middleware.ts) unless NEXT_PUBLIC_RESEARCH_SURFACE=1 (docs/RESEARCH-SURFACE.md). The nav is now Home, Matches, Film Room, Game Model, Training, Lineup, Match Lab + More (Training actions, Match Plans, Account, Admin); the Research group renders only with the switch on. / is a coach-promise landing that fetches nothing. Match Lab hides league entry points and tracking-only library entries. Eight research browser specs skip themselves when the switch is off; CI runs them with it on so the stash cannot rot.
  • Research-only components (38 files with their stylesheets and tests) moved to apps/web/components/research/; the shared rendering engine stays in components/. scripts/production-walk.mjs now expects 404 from the stashed routes instead of visiting them.
  • API: research routers gated. competition_routes, entity_routes, event_routes, provider_ingestion_routes, season_query_routes, opponent_prep_routes and data_platform_routes mount only when TOUCHLINE_RESEARCH_SURFACE=1; tier3 (Kickoff Pulse ingest), coaching workflow and recurring issues stay. The data-refresh cron is in-process and unaffected. test_research_surface_gate.py checks both states.
  • Renamed the production Railway project from touchline-ai to touchline360 (dashboard-only action; no rename capability exists across the Railway MCP tools, its own AI agent, or the CLI). Updated the 5 remaining references: .github/workflows/provision-railway.yml's RAILWAY_PROJECT_NAME, deployment/README.md, railway/README.md, docs/PULSE-UNIFICATION.md, and docs/BRAND-GUIDE.md's migration inventory. No services, deployments, or the touchline-media bucket were touched; verified all 7 services still report SUCCESS after the rename.
  • Fixed the flaky review-and-film.spec.ts e2e test ("authors, reorders and presents a teaching sequence"): it cold-compiles three dev-server routes in sequence, which could exceed the default 30s test timeout in isolation. Added an explicit 60s test.setTimeout. Verified 3/3 passing in isolation.
  • Added a "Suite" nav group (apps/web/lib/navigation.ts SUITE_PRODUCTS, apps/web/components/AppShell.tsx SuiteGroup) listing all 7 Touchline360 products. Select XI links to its live site (select-xi.pro); Kickoff Pulse links only when NEXT_PUBLIC_KICKOFF_PULSE_URL is set; Groundwork links to /data-health; the rest show a Live/Planned status only.
  • Extended scripts/release.mjs to also sync apps/api/app/main.py's FastAPI version, so /health always reports the same version as the rest of the release. Fixed a bug in the script's own CHANGELOG-parsing regexes, which didn't account for this repo's CRLF line endings and would have made every future release silently fail with "No [Unreleased] section found."

Tests

  • New Playwright coverage in shell-navigation.spec.ts asserting the Suite group renders all 7 products and expands/collapses correctly.
  • New unit assertions in navigation.test.ts for SUITE_PRODUCTS' hrefs.

0.6.1

Fixed

  • Debrief: seven components still hardcoded the old "TOUCHLINE AI" wordmark (route error states, the landing, directory, comparison, data-health and coaching-productivity headers); they now derive it from PRODUCT_BRAND.
  • API: /health reported a hardcoded version; it now reports the application version, which now tracks the suite version (0.6.1).

0.6.0

Added

  • Debrief: Training Planner. The training and coaching-intervention layer under /training and /api/training: coaching priorities, training weeks (microcycles labelled MD+1 … MD-1, proposed from the previous and next match), sessions as a live 8.5 × 11 document with a structured timeline, parallel goalkeeper blocks, constraints, attendance and drill feasibility mathematics, a structured drill-diagram editor (drag, keyboard, undo/redo, AI draw and modify), a built-in exercise library with per-coach favourites and saved blocks, AI session drafting that previews before anything is saved and falls back to a deterministic composer without a model provider, Run Session with an on-field timer and planned-versus-actual recording, post-session reflections, comments, revision history with restore, revocable share links, US Letter PDF and print, JSON/CSV export, a searchable session archive and coaching-memory questions answered only from stored records. Migration 021_training_planner.sql; documented in docs/TRAINING-PLANNER.md. Training is now a primary navigation item and the finding drawer's action reads "Add to Training".
  • Debrief: Next-Match Watch resolution. Watches resolve against the match actually played (watch_resolution.py) and persist one verdict per watch.
  • Deployment: embedded model. Production runs an embedded Ollama model on Railway (qwen2.5:1.5b derived as touchline, 8-thread cap); see docs/DEPLOYMENT.md.
  • Repository tooling: ESLint configuration for apps/web, the scripts/release.mjs version roll (pnpm release <patch|minor|major>), and this changelog.

Changed

  • Renamed the GitHub repository from touchline-ai to touchline360 and updated in-repo references (package name, GitHub repo paths, adapter user agent) to match.
  • Renamed the coaching product from Touchline to Debrief in all user-facing UI text (apps/web/lib/productBrand.ts and the hardcoded wordmark/footer strings that didn't derive from it). Internal identifiers — package name, API routes, CSS classes, env vars, DB schema — still say touchline, matching the existing convention that brand names and internal names are deliberately decoupled.
  • Renamed the Touchline Football Data Platform to Groundwork in user-facing docs and module docstrings. The apps/data_platform/ path and Python package name are unchanged for the same reason.
  • Documented the full suite structure: Trequa (company) → Touchline360 (suite) → Debrief, Groundwork, Kickoff Pulse, Select XI, ElevenIQ, Third Runner, Methodology (individual products).