League Game Widget WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
Word press plugin to deal with displaying our league tables
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/dbinterz/lgw-division-widget/releases/download/v2026.31.9/lgw-division-widget-v2026.31.9.zipA WordPress plugin that renders mobile-friendly league tables and fixtures for lawn bowls and other league sports, powered by published Google Sheets CSV data.
Features
- 📱 Mobile responsive — sticky Pos and Team columns, compact fixture layout on small screens
- 🏆 Promotion & relegation zones — colour coded with ▲/▼ symbols for accessibility
- ✅ Clinched detection — automatic shading when promotion/relegation is mathematically confirmed
- 🏅 Club badges — upload logos via WordPress Media Library, mapped to team names
- 🖱 Team modal — click any team name to see their full record and fixture list
- 🖨 Print views — print button on league table, fixtures, and team modal
- 🌙 Dark mode — auto-follows device/OS setting with manual toggle, preference remembered per device
- 💰 Sponsor logos — primary sponsor above title, additional sponsors rotate randomly below table
- ⚡ Server-side caching — configurable cache duration to speed up page loads
- 🔄 GitHub auto-updates — WordPress update notifications direct from GitHub releases
- 🔍 Team name validation — scorecard form checks team names and fixture pairings against the division CSV, with one-click correction for swapped home/away or missing suffixes (e.g. "Belmont" → "Belmont A")
Installation
- Download the latest release zip from Releases
- In WordPress go to Plugins → Add New → Upload Plugin
- Upload the zip and activate
- Go to Settings → LGW Widget to configure badges and sponsors
Usage
Add a shortcode block to any page:
[lgw_division csv="YOUR_CSV_URL" title="Division 1" promote="2" relegate="2"]
Shortcode Parameters
| Parameter | Description | Required |
|---|---|---|
csv |
Published Google Sheets CSV URL | ✅ Yes |
title |
Heading shown above the widget | No |
promote |
Number of promotion places | No |
relegate |
Number of relegation places | No |
sponsor_img |
Override primary sponsor image for this division | No |
sponsor_url |
Override primary sponsor link for this division | No |
sponsor_name |
Override primary sponsor alt text for this division | No |
Getting the CSV URL
- In Google Sheets go to File → Share → Publish to web
- Select the sheet, choose CSV format, click Publish
- Copy the URL and use it as the
csvparameter
Settings
Go to Settings → LGW Widget to manage:
- Sponsors — add logos with links; first sponsor appears above the title, others rotate below the table
- Club Badges — map team names (as they appear in the sheet) to badge images
- Cache Settings — configure how long data is cached (default 5 minutes)
- Plugin Updates — force an immediate check for updates from GitHub
- Clear Cache — force all divisions to fetch fresh data on next load
Google Sheet Structure
The plugin expects two sections in the sheet:
League Table
A section with a LEAGUE TABLE header row, followed by column headers starting with POS, then team data rows.
Fixtures
A section with a FIXTURES header row, followed by a column header row containing HPts, HTeam, HScore, AScore, ATeam, APts, then date rows and fixture rows.
Scorecard Submission
The [lgw_submit] shortcode adds a scorecard entry page for clubs.
Setup
- Go to Settings → LGW Widget and add each club with a passphrase under Score Entry — the what3words address for the clubhouse makes a good default (e.g.
filled.count.ripen) - Add an Anthropic API key under API Settings if you want AI photo parsing
- Create a page with
[lgw_submit]— clubs visit this page to submit scorecards
How it works
- Clubs log in with their club name and passphrase (no WordPress account needed) — passphrase entry is case-insensitive
- Three entry methods: photo (AI reads the scorecard image), Excel (upload the LGW template), or manual
- First submission sets status to Pending — awaiting confirmation from the other club
- Second club can Confirm (scores agree → Confirmed ✅) or Amend (scores differ → Disputed ⚠️)
- League admin resolves disputes via wp-admin → Scorecards
- Confirmed scorecards appear when clicking a played fixture row in the league table
Excel template
The plugin parses the standard LGW scorecard Excel template. Cells with unresolved formulas (total shots) are handled automatically by summing rink scores as a fallback.
Changelog
[2026.31.9]
Changed
- Stripe Checkout session creation is now idempotent. Both
lgw_entry_stripe_create_session()(single entry) andlgw_entry_stripe_create_batch_session()(bulk basket) now send anIdempotency-Keyheader on thePOST /v1/checkout/sessionscall —lgw-entry-<id>-<amount>andlgw-batch-<batch>respectively. A retried request (e.g. after a dropped response) returns the same Checkout Session instead of creating a second one, so a club can't be double-charged; the key includes the amount/batch so a genuine re-quote is still distinct. Aligns the outbound leg with the Stripe "accept a one-time payment" blueprint (product/price → Checkoutmode=payment→checkout.session.completedwebhook); the inbound webhook + the full outbound request shape are both proven offline viarest_do_request+pre_http_requestinterception. No behaviour change when Stripe isn't configured.
[2026.31.8]
Fixed
- Scorecard fuzzy name suggestions no longer capped at 3. Both "Did you mean:" surfaces in
lgw-scorecard.js— the import/review issue resolver (.lgw-ir-accept) and the inline new-name dialog (.lgw-npd-suggest) — hard-slicedsuggestions.slice(0, 3), so with more than three similar roster names the correct one could be truncated off before the user saw it.lgwFuzzyPlayerMatch()returns only exact-category matches (initial ↔ full first name, nickname, Levenshtein ≤ 2 typo), not noisy fuzz, so the cap was arbitrary truncation rather than ranking. Introduced a sharedLGW_MAX_NAME_SUGGESTIONS = 8constant used at both sites; flex-wrap keeps the list usable on mobile.
[2026.31.7]
Changed
- Multi-competition bulk entry — one club, one basket, one payment.
[lgw_champ_bulk_entry champ="a,b,c"]now accepts a comma/space list of championships and renders a single form: one club selector, one contact field, a per-competitionentries[<champ>]textarea (only competitions whose entry window is open are shown; closed ones listed as skipped), and one global "Enter & pay" button.lgw_ajax_entry_bulk_submitreadschamps(with legacy singlechamp/scalarentriesfallback), assigns one shared batch id across all competitions, creates ledger rows per comp (free → confirmed+projected+emailed immediately; paid →pending_payment), then opens a single Stripe Checkout over every paid entry vialgw_entry_stripe_create_batch_session(). That session now titles each line item by the entry's own championship (per-entrytitle_cache) so the receipt itemises across competitions. Guard: mixed currencies across paid comps are rejected (one Stripe session = one currency). Admin notification consolidated intolgw_entry_notify_admins_bulk_multi()— one email grouped by competition. Webhook confirmation (lgw_entry_stripe_webhook_finish_batch) was already batch-keyed and champ-agnostic, so it confirms the whole multi-comp batch unchanged. Single-championship usage is unchanged. - Live basket total on the bulk form. Each competition textarea carries
data-fee/data-cur/data-need; inline JS counts valid entries (mirroringlgw_entry_parse_bulk— newline units, singles comma-split, teams needing N/-separated members), renders a per-competition subtotal, sums fees per currency into a basket line, and reflects the grand total in the submit button label ("Enter & pay £43.00"). Recomputes on every keystroke; no server round-trip.
Fixed
- Entry-form theming. Entry cards (
[lgw_champ_entry]/[lgw_champ_bulk_entry]) now carry algw-entry-cardclass pinned to the light palette inlgw-scorecard.css(mirroring the existing.lgw-ca-boxrule), so@media(prefers-color-scheme:dark)no longer flips only the card to#1e1e2eagainst a light page (which also left grey helper text unreadable). Entry cards are pinned to the plugin's Saira font (.lgw-entry-cardselectors), and both entry shortcodes now enqueue thelgw-sairaGoogle Font (and add it as algw-scorecardstyle dependency) so entry-only pages actually load it — previously the bulk textareas weremonospaceand the rest of the card fell back to the site theme font (Manrope).
[2026.31.6]
Added
- Reactivate an archived season. New
lgw_revert_seasonPOST handler inlgw-seasons.php+ a "⏮ Make Active" button on each archived-season row. Demotes the current active season to archived (active => false) and promotes the chosen season (active => true), then re-sorts (active first, then descending by ID) and callslgw_seasons_sync_to_drive()so Drive/Sheets writeback and Quick Score Entry follow the reactivated season's divisions. Guarded: the target must exist and be archived (never the active one); scorecardlgw_sc_seasontags are left untouched (no un-stamping). Confirmation dialog on click;reverted=1success notice;not_founderror path.
[2026.31.5]
Added
- Bulk club entry (club admins only). New
[lgw_champ_bulk_entry champ="..."]shortcode +lgw_entry_bulk_submitAJAX.lgw_entry_parse_bulk()parses a textarea: entries newline-separated (singles also comma-separated); team members within an entry separated by/→ mapped to the canonicalA & B, Clubvialgw_entry_build_string(). Gated bylgw_entry_user_may_bulk()(approved club admin vialgw_user_can_submit_for(), ormanage_options); the club picker only lists clubs the user administers. Duplicates and wrong-member-count lines are skipped and reported. Free champs confirm + project immediately; paid champs use one combined Stripe Checkout for the whole batch —lgw_entry_stripe_create_batch_session()(one line-item per entry, tagged with algw_entry_batchid), andlgw_entry_stripe_webhook_finish_batch()confirms every pending row in the batch together after re-verifying the paid total against the sum of the batch's ledger amounts (idempotent, client-amount never trusted). - Player-list validation / capitation warning.
lgw_entry_unknown_players()checks each entered name against the club's tracked players (lgw_playerstable, matched vialgw_clean_player_name()); unmatched names are returned to the entrant as a non-blocking warning ("unregistered players may affect your club's capitation fees") and audit-logged on the entry. Applies to both single and bulk entry. Degrades to a no-op if the players module is absent, so it never blocks entry.
[2026.31.4]
Added
- Common championship engine abstraction. New module
lgw-champ-engine.phpintroduces theLGW_Champ_Engineinterface with two adapters —LGW_Gchamp_Engine(new group-knockout storelgw_gchamp_<id>) andLGW_Champ_Engine_Legacy(section-bracket storelgw_champ_<id>) — plus a resolverlgw_champ_engine($id),lgw_champ_engine_list()and sharedlgw_champ_engine_norm(). Callers speak one vocabulary (exists/get/draw_started/append_entry/remove_entry/list) and never branch on the storage back-end. Adding a future engine = one new class, zero caller edits.Changed
- Entry form now supports the legacy champ engine, not just gchamp.
lgw-entry.phpwas hard-wired tolgw_gchamp_*; the real NIPGL championships run on the legacylgw_champ_*engine, so[lgw_champ_entry champ="..."]returned "Championship not found" for all of them (#28).lgw_entry_get_champ(),lgw_entry_project(),lgw_entry_unproject(),lgw_entry_norm()and the admin championship picker now route throughlgw-champ-engine.php. Legacy engine: appends toentries[]pre-draw only (draw-started detected viasection_*_draw_version/*_draw_in_progress/final_*); sections are not rebuilt on append (the champ draw step rebuilds fromentries[], andlgw_champ_build_sections()shuffles); no preferences (old engine has none).lgw-champ.php/lgw-gchamp.phpare unchanged — the adapters wrap their storage from outside.
[2026.31.3]
Added
- Championship entry form + ledger. New module
lgw-entry.phpintroduces thelgw_entryCPT (the ledger: players, club, discipline, statuspending_payment|paid|confirmed|withdrawn|refunded, amount, payment ref, audit log) and the[lgw_champ_entry champ="..."]front-end shortcode. Players self-enter (login-gated) instead of an admin pasting into the gchamp textarea. A confirmed/paid entry is projected into the existinglgw_gchamp_<id>.entries[](+entry_preferenceskeyed by the entry string) bylgw_entry_project(), so the draw/bracket engine is untouched. Pre-draw only; post-draw entries are flagged for manual placement. - Per-club entry policy. Each club record gains an
entry_policy(open= players may self-enter |club_admin= approved club admins only), resolved bylgw_entry_policy_for_club()with precedence: per-champ override → per-club → league default optionlgw_entry_default_policy. Approved-admin checks reuselgw_user_can_submit_for(). - Stripe Checkout (raw REST, no SDK). When a champ has a fee,
lgw_entry_stripe_create_session()creates a Checkout Session viawp_remote_postand the entrant is redirected to hosted checkout. Aregister_rest_route('lgw/v1','/stripe-webhook')endpoint verifies theStripe-SignatureHMAC manually, re-checks amount/currency, and flipspending_payment → paididempotently. Secrets via options orLGW_STRIPE_SECRET_KEY/LGW_STRIPE_WEBHOOK_SECRETconstants. Free entries confirm immediately; admins can mark paid offline. - LGW → Entries admin screen. Per-champ entries list with Confirm / Mark paid / Withdraw / Refund row actions, per-champ fee/deadline/capacity/policy config (option
lgw_entry_cfg_<champ>, kept separate from the gchamp option), and a Stripe/policy settings box.
[2026.31.2]
Fixed
- Orphaned concessions could not be cleared from the fixture modal. The normal concession clear path is keyed on
home||away||date; postponing + rescheduling a conceded fixture changes the row date, so the modal's computed key no longer matched the concession's stored key (original date). Result: the overlay entry (lgw_concessions), auto-created 50-0 scorecard (lgw_sc_concession=1), score override (lgw_score_overrides) and cached fixture were stranded with no UI to remove them (e.g. a phantom confirmed result showing 0-0).Added
- Force-clear concession admin button in the fixture modal (played and unplayed views). New AJAX
lgw_force_clear_concession(noncelgw_submit_nonce,manage_options) matches by team names across all dates and wipes every concession artifact for the pair — overlay entries, concession scorecards, score overrides — then wipes the cached fixture back to unplayed. Idempotent; page reloads on success.
[2026.31.1]
Added
- Conditions of Play tab on
[lgw_finals](📜). A fourth tab beside competition / date / scoreboard rendering admin-editable rich text, stored per season as optionlgw_finals_conditions_<season>and seeded fromlgw_finals_conditions_default()(IBA Championships Stage 1 & 2 Conditions of Play 2026). Admins get an inline edit toggle → textarea → AJAXlgw_finals_save_conditions(noncelgw_finals_nonce,manage_options,wp_ksesagainstlgw_finals_conditions_allowed_html()—h3/h4/p/ul/ol/li/strong/em/aetc.). Tab choice persists inlocalStoragealongside the existing sort.
[2026.31.0]
Changed
- Ghost unlit-segment now visible on completed finals. The
::before "88"ghost applied to both live and final all along, butcolor: inheritat 13% washed out the green final digits. Switched to explicit rgba — amberrgba(255,107,26,.16)for live, greenrgba(72,224,138,.2)for final — so finished games keep the lit-panel look.
[2026.30.23]
Added
- DSEG7 seven-segment webfont on the LED scoreboard (SIL OFL, bundled under
fonts/dseg/; two@font-faceweights,url()resolved relative tolgw-finals.css). Digits render as a real 7-seg display, with a ghost unlit-segment layer — a faint all-lit88via::beforeat 13% opacity behind the live/final numbers. - Club names on the scoreboard — a sub-line under each player/team name (parsed from the
Name, Clubstring), uppercase grey.Changed
- Long team names wrap on the board instead of one-line ellipsis.
<wbr>break points injected after each/, so pairs/triples split player-per-line under a tight card;.lgw-finals-led-teamusesoverflow-wrap: anywhere.
[2026.30.22]
Added
- LED scoreboard view on the Finals Week page. New third tab (
📟 Scoreboard) beside "By competition" / "By date & rink" renders an old-style dark-panel LED board of summary scores across every scheduled match — live matches first (amber glowing digits, pulsing● LIVE · END n), then upcoming, then finished (greenFINAL). Score cells carry stable ids (#lgw-led-<mid>-h/-a/-s) and refresh through the same update path as the rest of the page: admin saves and the public 30s poll both flow throughupdateScoreBlock(), which now mirrors totals onto the board viaupdateLed(). Tab choice persists inlocalStoragealongside the existing sort. Pure-additive: no scoring/data-model changes.
[2026.30.21]
Added
- Live finals scoring is now an ordered log of checkpoints + ends (freely mixable). Two item kinds stored in
$match['ends']:- end — a delta pair
[h, a](+ Add end); running += delta, end# = prev + 1. - checkpoint —
⚡ Update score: an absolute score declared at end N, stored as the delta needed to reach it (['0'=>dh,'1'=>da,'sum'=>1,'end'=>N]), sosum(item[0]/item[1])is still the running total everywhere (poll, complete, finals-started). The running total jumps to a checkpoint's absolute; it is not a new base to re-add ends onto. - Example: Update 14-10@12 → Add 3-0 (17-10, end 13) → Update 18-14@16 (jump, new line) → Add 0-2 (18-16, end 17).
- New helpers
lgw_finals_score_items()(reads the log; migrates legacy 2026.30.20/.21live_home/away/endsbaseline into a leading checkpoint) andlgw_finals_fold_items()(folds to display rows + running totals + current end).apply_end_action/scoring_response/render_ends_table/render_scoring_areaall rebuilt around them;set_totalappends a checkpoint delta;resetclears the log.Changed
- end — a delta pair
- Rendering fully centralised server-side. The scoring area (table + toolbar) is produced only by
lgw_finals_render_scoring_area()and returned as an HTML fragment by thesave_endhandlers and the live poll; the client-siderenderEndsTable()mirror is removed.updateScoreBlock()uses the server's authoritative totals. Poll now returnshtml+homeTotal/awayTotal/curEnd/isLive.Fixed
- Adding an end no longer drops earlier scoring from the displayed total (superseded by the log model + server-authoritative totals).
[2026.30.20]
Added
- Summary (quick-score) live mode + Reset, for all finals competitions. A match can be scored end-by-end (detailed) or by overall updates (⚡ Quick score, stored as
live_home/live_awayon the match slot, mutually exclusive withends). New ↺ Reset clears all ends / the summary score back to "not started" (schedule untouched). Toolbar:+ Add end,⚡ Quick score,↺ Reset,✓ Complete game. - The scoring-area markup is now produced by one server helper
lgw_finals_render_scoring_area()(three states: detailed / summary / not-started), and thesave_endAJAX handlers (gchamp + standard) return that HTML fragment plus totals via shared helperslgw_finals_apply_end_action()+lgw_finals_scoring_response(), so the browser swaps in the fragment instead of rebuilding it — no client/server divergence.end_actiongainsresetandset_total; the render/score-block/status-class use a unified$is_live(ends or summary total).
[2026.30.19]
Fixed
- "Enter final score" button dead after starting live scoring. In
saveEnd()the order was: re-render ends table →bindMatchButtons()→updateScoreBlock(). ButupdateScoreBlock()replaces the score-block markup (including the edit-score ✏️ button), so the freshly-created button was never bound and looked disabled. NowupdateScoreBlock()runs first andbindMatchButtons()binds the whole match element afterward.
[2026.30.18]
Fixed
- Rename dropdown now surfaces names lingering only in the finals.
lgw_gchamp_collect_all_entries()(the source for the rename dropdown) didn't scanfinals_matchesordays[*]['ko_qualifiers'], so an entry renamed before 2026.30.17 — whose old spelling survived only in those finals structures — couldn't be selected to correct it. Both are now collected, so the old name reappears in the dropdown and a resave (old → correct) patches the leftover finals copies (2026.30.17 already renames those structures on save).
[2026.30.17]
Fixed
- Group Championship entry rename now propagates to the Finals Week bracket.
lgw_ajax_gchamp_rename_entryrenamed entries, group fixtures, per-dayko_bracket,days[*]['qualifiers'], and the top-level bracket/qualifiers — but notdays[*]['ko_qualifiers'](the name sourcelgw_gchamp_finals_slots()seeds from) norfinals_matches[*]['home'/'away'](the finals snapshot the finals page reads directly). A corrected name therefore updated the group stage but still showed the old spelling in the finals. Both are now renamed (finals via the existing flat-matches helper). Verified: a rename of an over55-pairs entry updates 1 finals slot + 1 ko_qualifiers entry that previously stayed stale.
[2026.30.16]
Added
- Per-match disc override. Beyond the whole-championship convention, admins can set a per-side disc colour for a single match from that match's date/time editor. Each side has a select with a
— Default —option (empty = inherit the convention). Stored asfinals_disc_home/finals_disc_awayon the individual match slot; the extendedsave_datetimehandlers (both gchamp inlgw-gchamp.phpand standard inlgw-finals.php) persist it and return the resolved effective colours, which the JS uses to swap the chips live.Changed
- Disc convention + override now work on every finals competition, not just Group Championships. The bulk header control and per-match overrides are available for standard championship finals (singles/pairs/triples/fours/…) too. The bulk control posts to a new generic
lgw_finals_set_discshandler (verifieslgw_finals_nonce) that routes tolgw_gchamp_<id>orlgw_champ_<id>via anis_gchampflag; the gchamp-onlylgw_gchamp_finals_set_discshandler from 2026.30.15 is removed in favour of it.
[2026.30.15]
Added
- Disc-colour chips on Finals Week matches. Each side shows a coloured dot + label (e.g. 🔴 Red / 🟡 Yellow) so viewers can identify entries on the green. Convention is set per championship (bulk) from the Finals page header via an admin "Discs: Home […] Away […] — Apply to all" control, stored as
finals_disc_home/finals_disc_awayon the gchamp option and applied to every match. Defaults Red (home) / Yellow (away). Palette: red, yellow, blue, green, orange, brown, black, white, pink. New AJAX handlerlgw_gchamp_finals_set_discs(verifieslgw_gchamp_scorenonce, validates slugs againstlgw_finals_disc_palette()).Fixed
- Club badges now display on Finals Week matches. The lookup lowercased the parsed club name but compared it against
lgw_club_badgeskeys stored in original case (N.I.C.S.,Ards), so no badge ever resolved. A lowercased key map is now built once for a case-insensitive match.
[2026.30.14]
Fixed
- "Match not found" (still) on gchamp finals edits + dead "by competition / by date" sort tabs — a syntax error introduced by 2026.30.13. The 2026.30.13 edit wrapped the finals body inline script but left a duplicate, unclosed
(function(){before the existing sort IIFE (double-open, single close). The whole `
This README is longer than the copy stored here. Read the rest on GitHub →
Releases
Newest 25 of 60 recorded releases. Each count is every asset in that release; expand a row for the breakdown.