From 4c16bbfe6895d1ec3a380d334c9d711118b4db43 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:36:11 +0100 Subject: [PATCH 1/9] Add FIFA World Cup 2026 plugin Monitors all 48 teams, match results, group standings, and knockout fixtures via the community worldcup26.ir API. Includes per-team perspective dashboard with next/last match, group points, and stats tiles. Co-Authored-By: Claude Sonnet 4.6 --- .github/CODEOWNERS | 1 + plugins/WorldCup2026/v1/configValidation.json | 11 ++ plugins/WorldCup2026/v1/custom_types.json | 9 + .../v1/dataStreams/scripts/group-standings.js | 76 ++++++++ .../v1/dataStreams/scripts/knockout.js | 35 ++++ .../v1/dataStreams/scripts/last-match.js | 51 ++++++ .../v1/dataStreams/scripts/matches.js | 62 +++++++ .../v1/dataStreams/scripts/next-match.js | 51 ++++++ .../v1/dataStreams/scripts/team-standing.js | 27 +++ .../worldcup2026-group-standings.json | 34 ++++ .../v1/dataStreams/worldcup2026-knockout.json | 26 +++ .../dataStreams/worldcup2026-last-match.json | 32 ++++ .../v1/dataStreams/worldcup2026-matches.json | 35 ++++ .../dataStreams/worldcup2026-next-match.json | 27 +++ .../worldcup2026-team-standing.json | 29 ++++ .../worldcup2026-teams-import.json | 23 +++ .../v1/defaultContent/manifest.json | 5 + .../v1/defaultContent/scopes.json | 17 ++ .../v1/defaultContent/team.dash.json | 164 ++++++++++++++++++ plugins/WorldCup2026/v1/docs/README.md | 30 ++++ plugins/WorldCup2026/v1/icon.svg | 39 +++++ .../v1/indexDefinitions/default.json | 22 +++ plugins/WorldCup2026/v1/metadata.json | 37 ++++ plugins/WorldCup2026/v1/ui.json | 1 + 24 files changed, 844 insertions(+) create mode 100644 plugins/WorldCup2026/v1/configValidation.json create mode 100644 plugins/WorldCup2026/v1/custom_types.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/matches.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json create mode 100644 plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json create mode 100644 plugins/WorldCup2026/v1/defaultContent/manifest.json create mode 100644 plugins/WorldCup2026/v1/defaultContent/scopes.json create mode 100644 plugins/WorldCup2026/v1/defaultContent/team.dash.json create mode 100644 plugins/WorldCup2026/v1/docs/README.md create mode 100644 plugins/WorldCup2026/v1/icon.svg create mode 100644 plugins/WorldCup2026/v1/indexDefinitions/default.json create mode 100644 plugins/WorldCup2026/v1/metadata.json create mode 100644 plugins/WorldCup2026/v1/ui.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2fa77256..cd076fbc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,6 +22,7 @@ plugins/SumoLogic/* @richpeters plugins/TransportForLondon/* @clarkd plugins/UniFi/* @adamkinniburgh plugins/UptimeRobot/* @kieranlangton +plugins/WorldCup2026/* @TimWheeler-SQUP # Fallback – if a plugin has no specified author diff --git a/plugins/WorldCup2026/v1/configValidation.json b/plugins/WorldCup2026/v1/configValidation.json new file mode 100644 index 00000000..53dba4d8 --- /dev/null +++ b/plugins/WorldCup2026/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Connect to worldcup26.ir", + "dataStream": { "name": "worldcup2026-teams-import" }, + "required": true, + "error": "Could not reach the worldcup26.ir API. Check your network connection.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/WorldCup2026/v1/custom_types.json b/plugins/WorldCup2026/v1/custom_types.json new file mode 100644 index 00000000..8ea11b96 --- /dev/null +++ b/plugins/WorldCup2026/v1/custom_types.json @@ -0,0 +1,9 @@ +[ + { + "name": "World Cup Team", + "sourceType": "World Cup Team", + "icon": "flag", + "singular": "Team", + "plural": "Teams" + } +] diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js new file mode 100644 index 00000000..b67c465f --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -0,0 +1,76 @@ +var games = data.games || []; +var teamFilter = context.config.team || ''; + +var groupGames = games.filter(function(g) { return g.type === 'group'; }); + +var standings = {}; + +function ensureTeam(name, group) { + if (!standings[name]) { + standings[name] = { team: name, group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, gd: 0, pts: 0 }; + } +} + +groupGames.forEach(function(g) { + ensureTeam(g.home_team_name_en, g.group); + ensureTeam(g.away_team_name_en, g.group); + + if (g.finished !== 'TRUE') return; + + var homeScore = parseInt(g.home_score, 10) || 0; + var awayScore = parseInt(g.away_score, 10) || 0; + var home = standings[g.home_team_name_en]; + var away = standings[g.away_team_name_en]; + + home.mp++; + away.mp++; + home.gf += homeScore; + home.ga += awayScore; + away.gf += awayScore; + away.ga += homeScore; + + if (homeScore > awayScore) { + home.w++; home.pts += 3; away.l++; + } else if (awayScore > homeScore) { + away.w++; away.pts += 3; home.l++; + } else { + home.d++; home.pts += 1; away.d++; away.pts += 1; + } +}); + +var rows = Object.keys(standings).map(function(k) { + var s = standings[k]; + var gd = s.gf - s.ga; + return { + team: s.team, + group: s.group, + mp: s.mp, + w: s.w, + d: s.d, + l: s.l, + gf: s.gf, + ga: s.ga, + gd: gd, + pts: s.pts + }; +}); + +if (teamFilter) { + var filterLower = teamFilter.toLowerCase(); + var matchedTeam = rows.filter(function(r) { + return r.team.toLowerCase().indexOf(filterLower) !== -1; + })[0]; + if (matchedTeam) { + var targetGroup = matchedTeam.group; + rows = rows.filter(function(r) { return r.group === targetGroup; }); + } +} + +rows.sort(function(a, b) { + if (a.group !== b.group) return a.group.localeCompare(b.group); + if (b.pts !== a.pts) return b.pts - a.pts; + if (b.gd !== a.gd) return b.gd - a.gd; + return b.gf - a.gf; +}); + +result = rows; diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js b/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js new file mode 100644 index 00000000..f483d4ba --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js @@ -0,0 +1,35 @@ +var games = data.games || []; + +var stageMap = { + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +result = games.filter(function(g) { return g.type !== 'group'; }).map(function(g) { + var homeTeam = g.home_team_id !== '0' ? g.home_team_name_en : (g.home_team_label || 'TBD'); + var awayTeam = g.away_team_id !== '0' ? g.away_team_name_en : (g.away_team_label || 'TBD'); + + var score = '-'; + if (g.finished === 'TRUE') { + score = g.home_score + '-' + g.away_score; + } else if (g.time_elapsed !== 'notstarted') { + score = g.home_score + '-' + g.away_score + ' (Live)'; + } + + var status = 'Upcoming'; + if (g.finished === 'TRUE') status = 'Finished'; + else if (g.time_elapsed !== 'notstarted') status = 'Live'; + + return { + date: g.local_date, + round: stageMap[g.type] || g.type, + home_team: homeTeam, + away_team: awayTeam, + score: score, + status: status + }; +}); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js new file mode 100644 index 00000000..f0cb83b8 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -0,0 +1,51 @@ +var games = data.games || []; +var teamName = context.objects[0] ? context.objects[0].name : ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +var played = games.filter(function(g) { + return g.finished === 'TRUE' && + (g.home_team_name_en === teamName || g.away_team_name_en === teamName); +}); + +played.sort(function(a, b) { + return parseDate(b.local_date) - parseDate(a.local_date); +}); + +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +if (played.length === 0) { + result = [{ date: 'No matches played yet', home_away: '', opponent: '', score: '', result: '', stage: '', sourceId: sourceId }]; +} else { + var last = played[0]; + var isHome = last.home_team_name_en === teamName; + var opponent = isHome ? last.away_team_name_en : last.home_team_name_en; + var myScore = parseInt(isHome ? last.home_score : last.away_score, 10); + var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10); + var scoreStr = isHome ? last.home_score + '-' + last.away_score : last.away_score + '-' + last.home_score; + var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw'; + + result = [{ + date: last.local_date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + score: scoreStr, + result: matchResult, + stage: stageMap[last.type] || last.type, + sourceId: sourceId + }]; +} diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js new file mode 100644 index 00000000..c1496400 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -0,0 +1,62 @@ +var games = data.games || []; +var team = context.config.team || ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +function getTeamName(game, side) { + if (side === 'home') { + return game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); + } + return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); +} + +var filtered = games; +if (team) { + var teamLower = team.toLowerCase(); + filtered = games.filter(function(g) { + var home = (g.home_team_name_en || '').toLowerCase(); + var away = (g.away_team_name_en || '').toLowerCase(); + return home.indexOf(teamLower) !== -1 || away.indexOf(teamLower) !== -1; + }); +} + +filtered.sort(function(a, b) { + return parseDate(a.local_date) - parseDate(b.local_date); +}); + +result = filtered.map(function(g) { + var score = '-'; + if (g.finished === 'TRUE') { + score = g.home_score + '-' + g.away_score; + } else if (g.time_elapsed !== 'notstarted') { + score = g.home_score + '-' + g.away_score + ' (Live)'; + } + + var status = 'Upcoming'; + if (g.finished === 'TRUE') status = 'Finished'; + else if (g.time_elapsed !== 'notstarted') status = 'Live'; + + return { + date: g.local_date, + home_team: getTeamName(g, 'home'), + away_team: getTeamName(g, 'away'), + score: score, + group: g.group, + stage: stageMap[g.type] || g.type, + status: status + }; +}); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js new file mode 100644 index 00000000..6b254b2d --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -0,0 +1,51 @@ +var games = data.games || []; +var teamName = context.objects[0] ? context.objects[0].name : ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +var teamGames = games.filter(function(g) { + return g.home_team_name_en === teamName || g.away_team_name_en === teamName; +}); + +var upcoming = teamGames.filter(function(g) { + return g.finished !== 'TRUE' && g.time_elapsed === 'notstarted'; +}); + +upcoming.sort(function(a, b) { + return parseDate(a.local_date) - parseDate(b.local_date); +}); + +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +if (upcoming.length === 0) { + result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }]; +} else { + var next = upcoming[0]; + var isHome = next.home_team_name_en === teamName; + var opponent = isHome ? next.away_team_name_en : next.home_team_name_en; + if (!opponent || opponent === 'undefined') { + opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD'); + } + result = [{ + date: next.local_date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + stage: stageMap[next.type] || next.type, + group: next.type === 'group' ? 'Group ' + next.group : '', + sourceId: sourceId + }]; +} diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js new file mode 100644 index 00000000..7537d35c --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -0,0 +1,27 @@ +var games = data.games || []; +var teamName = context.objects[0] ? context.objects[0].name : ''; + +var groupGames = games.filter(function(g) { + return g.type === 'group' && + (g.home_team_name_en === teamName || g.away_team_name_en === teamName); +}); + +var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0; + +groupGames.forEach(function(g) { + if (g.finished !== 'TRUE') return; + mp++; + var isHome = g.home_team_name_en === teamName; + var myScore = parseInt(isHome ? g.home_score : g.away_score, 10) || 0; + var oppScore = parseInt(isHome ? g.away_score : g.home_score, 10) || 0; + gf += myScore; + ga += oppScore; + if (myScore > oppScore) w++; + else if (myScore < oppScore) l++; + else d++; +}); + +var pts = (w * 3) + d; + +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; +result = [{ mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, sourceId: sourceId }]; diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json new file mode 100644 index 00000000..b1020b4f --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json @@ -0,0 +1,34 @@ +{ + "name": "worldcup2026-group-standings", + "displayName": "Group Standings", + "description": "Group stage standings calculated from match results, with optional filter to a specific team's group", + "tags": ["Standings"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "group-standings.js" + }, + "ui": [ + { + "label": "Filter by Team (leave blank for all groups)", + "name": "team", + "type": "text", + "defaultValue": "" + } + ], + "matches": "none", + "timeframes": false, + "metadata": [ + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "team", "displayName": "Team", "shape": "string", "role": "label" }, + { "name": "mp", "displayName": "MP", "shape": "number" }, + { "name": "w", "displayName": "W", "shape": "number" }, + { "name": "d", "displayName": "D", "shape": "number" }, + { "name": "l", "displayName": "L", "shape": "number" }, + { "name": "gf", "displayName": "GF", "shape": "number" }, + { "name": "ga", "displayName": "GA", "shape": "number" }, + { "name": "gd", "displayName": "GD", "shape": "number" }, + { "name": "pts", "displayName": "Pts", "shape": "number" } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json new file mode 100644 index 00000000..f787afe2 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json @@ -0,0 +1,26 @@ +{ + "name": "worldcup2026-knockout", + "displayName": "Knockout Matches", + "description": "All knockout stage fixtures, showing placeholder labels until teams are determined", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "knockout.js" + }, + "matches": "none", + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "round", "displayName": "Round", "shape": "string" }, + { "name": "home_team", "displayName": "Home Team", "shape": "string" }, + { "name": "away_team", "displayName": "Away Team", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { + "name": "status", + "displayName": "Status", + "shape": ["state", { "map": { "success": ["Finished"], "warning": ["Live"], "unknown": ["Upcoming"] } }] + } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json new file mode 100644 index 00000000..00019cac --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json @@ -0,0 +1,32 @@ +{ + "name": "worldcup2026-last-match", + "displayName": "Last Match", + "description": "The most recently completed match for the selected team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "last-match.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_away", "displayName": "Home / Away", "shape": "string" }, + { "name": "opponent", "displayName": "Opponent", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { + "name": "result", + "displayName": "Result", + "shape": ["state", { "map": { "success": ["Win"], "error": ["Loss"], "unknown": ["Draw"] } }] + }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json new file mode 100644 index 00000000..6a217b83 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json @@ -0,0 +1,35 @@ +{ + "name": "worldcup2026-matches", + "displayName": "Matches", + "description": "All 104 World Cup 2026 fixtures in date order, with optional filter by team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "matches.js" + }, + "ui": [ + { + "label": "Filter by Team (leave blank for all)", + "name": "team", + "type": "text", + "defaultValue": "" + } + ], + "matches": "none", + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_team", "displayName": "Home Team", "shape": "string" }, + { "name": "away_team", "displayName": "Away Team", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { + "name": "status", + "displayName": "Status", + "shape": ["state", { "map": { "success": ["Finished"], "warning": ["Live"], "unknown": ["Upcoming"] } }] + } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json new file mode 100644 index 00000000..6768ee4b --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json @@ -0,0 +1,27 @@ +{ + "name": "worldcup2026-next-match", + "displayName": "Next Match", + "description": "The next upcoming match for the selected team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "next-match.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_away", "displayName": "Home / Away", "shape": "string" }, + { "name": "opponent", "displayName": "Opponent", "shape": "string" }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json new file mode 100644 index 00000000..2ca61154 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json @@ -0,0 +1,29 @@ +{ + "name": "worldcup2026-team-standing", + "displayName": "Team Standing", + "description": "Group stage standing and match statistics for the selected team", + "tags": ["Standings"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/get/games", + "postRequestScript": "team-standing.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "mp", "displayName": "Played", "shape": "number" }, + { "name": "w", "displayName": "Won", "shape": "number" }, + { "name": "d", "displayName": "Drawn", "shape": "number" }, + { "name": "l", "displayName": "Lost", "shape": "number" }, + { "name": "pts", "displayName": "Points", "shape": "number" }, + { "name": "gf", "displayName": "Goals For", "shape": "number" }, + { "name": "ga", "displayName": "Goals Against", "shape": "number" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json new file mode 100644 index 00000000..e841e863 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json @@ -0,0 +1,23 @@ +{ + "name": "worldcup2026-teams-import", + "displayName": "Teams (Import)", + "description": "All 48 World Cup 2026 teams", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/get/teams", + "pathToData": "teams" + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "shape": "string" }, + { "name": "name_en", "displayName": "Team", "shape": "string", "role": "label" }, + { "name": "fifa_code", "displayName": "FIFA Code", "shape": "string" }, + { "name": "flag", "displayName": "Flag", "shape": "url" }, + { "name": "groups", "displayName": "Group", "shape": "string" }, + { "name": "iso2", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/defaultContent/manifest.json b/plugins/WorldCup2026/v1/defaultContent/manifest.json new file mode 100644 index 00000000..cb9c715d --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/manifest.json @@ -0,0 +1,5 @@ +{ + "items": [ + { "name": "team", "type": "dashboard" } + ] +} diff --git a/plugins/WorldCup2026/v1/defaultContent/scopes.json b/plugins/WorldCup2026/v1/defaultContent/scopes.json new file mode 100644 index 00000000..000d47dc --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/scopes.json @@ -0,0 +1,17 @@ +[ + { + "name": "All Teams", + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "variable": { + "name": "Team", + "type": "object", + "default": "none", + "allowMultipleSelection": false + } + } +] diff --git a/plugins/WorldCup2026/v1/defaultContent/team.dash.json b/plugins/WorldCup2026/v1/defaultContent/team.dash.json new file mode 100644 index 00000000..4b7ee5d3 --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/team.dash.json @@ -0,0 +1,164 @@ +{ + "name": "Team Dashboard", + "schemaVersion": "1.4", + "variables": [ + "{{variables.[Team]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "9d2b7a31-0998-43fa-8479-29e61e6e9ef8", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Next Match", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-next-match}}", + "name": "worldcup2026-next-match", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["date", "home_away", "opponent", "stage", "group"] + } + } + } + } + }, + { + "i": "81d998b7-a744-4abd-8284-5e8b2bf5f8af", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Last Match", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-last-match}}", + "name": "worldcup2026-last-match", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["date", "home_away", "opponent", "score", "result", "stage"] + } + } + } + } + }, + { + "i": "b4b8fd8e-358b-46b2-9d8e-dabfb2fdff87", + "x": 0, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Group Points", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-team-standing}}", + "name": "worldcup2026-team-standing", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["pts", "mp", "w", "d", "l"], + "hiddenColumns": ["gf", "ga", "gd"] + } + } + } + } + }, + { + "i": "5b8537bf-dd49-4d1a-a4b0-401b24d5bf22", + "x": 2, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Stats", + "description": "Total goals for, against and difference", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-team-standing}}", + "name": "worldcup2026-team-standing", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["gf", "ga", "gd"], + "hiddenColumns": ["pts", "mp", "w", "d", "l"] + } + } + } + } + } + ] + } +} diff --git a/plugins/WorldCup2026/v1/docs/README.md b/plugins/WorldCup2026/v1/docs/README.md new file mode 100644 index 00000000..eab872a2 --- /dev/null +++ b/plugins/WorldCup2026/v1/docs/README.md @@ -0,0 +1,30 @@ +# Before you start + +This plugin connects to the community API at [worldcup26.ir](https://worldcup26.ir) — an unofficial, community-maintained data source. It is not an official FIFA API. Data coverage and availability depends on the third party keeping the service running. + +No account, API key, or credentials are required. + +## What this plugin monitors + +Indexes all 48 FIFA World Cup 2026 teams into SquaredUp, making them available for search, scoping, and dashboard variables. Data streams cover: + +- Live and completed match results +- Group stage standings +- Knockout bracket fixtures +- Per-team statistics (next match, last match, group points) + +## What gets indexed + +| Object type | Description | +|---|---| +| World Cup Team | All 48 participating national teams, including FIFA code, group, and flag | + +## Configuration + +No configuration fields are required. Add the plugin and it connects immediately. + +## Known limitations + +- Data is sourced from an unofficial community API (`worldcup26.ir`) and may be delayed, incomplete, or unavailable during high-traffic periods +- Knockout stage fixtures show placeholder labels (e.g. "Winner Group A") until the group stage is complete and teams are determined +- Match times are in local event time — no timezone conversion is applied diff --git a/plugins/WorldCup2026/v1/icon.svg b/plugins/WorldCup2026/v1/icon.svg new file mode 100644 index 00000000..773403b3 --- /dev/null +++ b/plugins/WorldCup2026/v1/icon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/WorldCup2026/v1/indexDefinitions/default.json b/plugins/WorldCup2026/v1/indexDefinitions/default.json new file mode 100644 index 00000000..4fb8b961 --- /dev/null +++ b/plugins/WorldCup2026/v1/indexDefinitions/default.json @@ -0,0 +1,22 @@ +{ + "steps": [ + { + "name": "teams", + "dataStream": { + "name": "worldcup2026-teams-import" + }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name_en", + "type": { "value": "World Cup Team" }, + "properties": [ + "fifa_code", + "flag", + { "group": "groups" }, + "iso2" + ] + } + } + ] +} diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json new file mode 100644 index 00000000..a60e828c --- /dev/null +++ b/plugins/WorldCup2026/v1/metadata.json @@ -0,0 +1,37 @@ +{ + "name": "worldcup2026", + "displayName": "FIFA World Cup 2026", + "version": "1.0.0", + "author": { + "name": "@TimWheeler-SQUP", + "type": "community" + }, + "description": "Live scores, group standings, knockout bracket, and team data for the FIFA World Cup 2026.", + "category": "Fun", + "type": "hybrid", + "schemaVersion": "2.0", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["world cup", "fifa", "football", "soccer", "2026"], + "objectTypes": ["World Cup Team"], + "links": [ + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/WorldCup2026/v1", + "label": "Repository" + }, + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/WorldCup2026/v1/docs/README.md", + "label": "Help adding this plugin" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "authMode": "none", + "baseUrl": "https://worldcup26.ir" + } + } +} diff --git a/plugins/WorldCup2026/v1/ui.json b/plugins/WorldCup2026/v1/ui.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/plugins/WorldCup2026/v1/ui.json @@ -0,0 +1 @@ +[] From 33e68242bfb739df920fad952b367e571b29cbbd Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:38:43 +0100 Subject: [PATCH 2/9] Replace generated icon with official FIFA World Cup 2026 logo Co-Authored-By: Claude Sonnet 4.6 --- plugins/WorldCup2026/v1/icon.png | Bin 0 -> 20115 bytes plugins/WorldCup2026/v1/icon.svg | 39 ------------------------------- 2 files changed, 39 deletions(-) create mode 100644 plugins/WorldCup2026/v1/icon.png delete mode 100644 plugins/WorldCup2026/v1/icon.svg diff --git a/plugins/WorldCup2026/v1/icon.png b/plugins/WorldCup2026/v1/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c370f771d419f88e99cdda3bd8e788da1723f3c3 GIT binary patch literal 20115 zcmV)MK)An&P)F?wcqXMP@sK_zo=BAfTYd433^Efc}E6$M+A38 z8-!NS<=h^9RLkPn7lBj7-_#R-P{7^Lg}kJGxu1BonrW?%Mw)f=`1VSjc=!AHTBL;f z{QG07i!F|0;_vAviC^36tW+M-6dDGKgg6@$EK|Y3uaxJCtqj_VYrP za#Ngraj=!K)Wr^cOuE|4@OgmiPFMc-_v=$!;sOuj1{2~05#tIL^{lY^*4XACEc@Er z<|Z-w-r)DSz2prV_OiC|frs&jjr5wJ@{W}3T4MhD`|V+B^OTw62Nd+6rSEQb{{8*# zX>sX0Mg8ya>Oo8W^YrRSQSWqn^`@%&($wV}C;80JwOo9{9k-`{Ly1D>nS* z=jbs!{Oj!KHb7=(WM^k)i;Ie8W@Tt-XZrg4Wo2ZEiHMh%mbJCC(b3T^E-cj4)G#nE z*Vos*y}X*5nYy~VA|fCi9UC4V9ILCUhlhq@Vqp3C`C4v)GE8FP<>)O)U)|y5D@0t{ z-{Z&5)D}8W2rotoFh>S0L=H1bAVOEw+TJHYTDii>88c0_zs9}A&5W9+8a-3Q%h4S- zQNYO0a)XnCk)Ncnxl(6(dyJZ#sk3f?kY;#^{QUf1bA&ffW#;MYJ5p%r?Cw8SYwhmu zLt1X{@bXYlPV@8gQc_WFZ*7y4lDN3HARr#Fu&~e1&nqh_%*@OvC@09s$fl;Hf`Wm6 ze}1E*qQ%9w!j4&TyW>U zxR$*W&K^^=V54!#lP#9#eUf(idCd&J8O16`+Iy*DV5eJenKlvGMyuW}1#;Wl{?t}G zQc*A$Nh*|UiBxVU+B9u)Wf;w9bvGEZAW5@P%8hifR5xfkBbo~i6iCuERxjb-soOL& zvy|y5m_U*Wy+pBZ(wvNbegF|ks3OZ3~j*Lg{U7HtD<7Q)NifxI!e=H|P@1 zX;yYN0!bQSh5S*QTyUGy%u4%^q;pCn86{UNnwz66yW55&IeM9{O|H0+ZnrqiPL?G( zD#4N=I8||$(|dhJv$Oj(ND@$J|SU&_S%V-|e4vr}bmY7t|M`;x6Z{_Oz zq%GbG0?2ThOHs$rtB-$TSxHJGV9*$LP_}Bx1A}qScXa@QLza-ag=1G9twCB&dOAwv z5*DEJg4sGNbucvXki{9NaorGxp51wR57IK~6ipI@HKaF^Z5*?*OwKMAdp=~_=4bn2Hq?#8e3?^rcan4=CIzMD_7R{G| zJ(CmXr!QsloJg=_t$o%s-|IC<%VSV9=o~k5nccuaxMj1U@vzPhS=Y-o5)qW)v>UgLl$S@Oc^I?rDCmsV@i>c zi)aKx5AJKKsy_V)X*qug^HEXUe#&Tw>%VX8};2sH!Y$Y1B&Og0pH20#=sU)%^lobfS#h zbUPTjp`LpB9?M2{<=`yPgFvR98o*UYFqUt%G4$&%>Z#{%;cZfAhXgp^*5NL)Z^4}s z7c5zM4847$si$79!^@;tm4vvN#{3dtPA;}F^z3&{J@e!lyh|EnX#y6lVSd4;TUnbr zfWw!nW8>i~c$HKs+B6Bv>6lYF7;t@7Qz~QVi|Wws-&NpEl9e=QBHl`1E>^s1GA{nH z@Ui+A?ZX3jlB8_4XbM&>V-DqHlW}qtj+|cVob&zfCcH>$hiMvaCNR6+*r#ElupF@5Jw7xfa~?IU=F)Y~C3&eND*(BL#dvjJfJf0{Z@TYUhJ zkd%~7BAoGh9P>*tnxr%U@a?{)UiNey-XJx?!o`Z2rDQ|L{I(fQva0~fW7UoCwgNAZ z^qgq1vZKYsK*4t?Xo3`g_1`ts-RjR+eA1TT7FIo>U>??}LY{LHiiQB(-B3lI9AeQ) zy%M=$-Jpdz>}LZbO2kRXUzoIDsoIBoSaed_AXluJ*D0B*(!^;t)d6t$Se-!I_=?3Q`ALXR zebi@#GJ%lra!?@L2JmfFQzv%w+?CoIl;}SQw)x zN}P|p0<6_JO&1uPY=r7GkFda`L5U_|r+BNI-Rmg0VBl{tPD3o~G^cMenrgEH;H@*l zn&8$8EG}ukNfYq%M-I&Br(y~mp-tx>S*eUO;$geP4#WVw_@X*IddQM8G%L<{H(z#o zJYauC7mdxl7aGMLhOXb&riXR^0Tz~|2WWPj*+m`4+FeEt-rC2R%9Op(Oja;-?~SI; z;2{>46zkGFc)c;+6>%d7M~3t1UHx`PoyqfOSX5Hnq(74o}bCrC5~yX8k=K4)H5@0qyfT7LnAb z)8DGO9f3pqxX*K!I#Rnue& zbyFP$7kzxE%^jBSD;ANI|NF9v6fok|6G1#Dyfq<}QuKE)OZiv$^9nJ;^3dTKq>6Vr6&FfEJzkR#@==m@1X??;XlKelqs(Lk3)8XRl zeeF_K_itT)dx&Govj;CXRy-4yv`asb+0Et~6$Ngd-5T#|MSc498P5HA_rr<|Ym^Mj zZgm66cua@LmHXq%)#}4_j8MM4TajTo ziFU1176m2neq`vbt=xJG==m_?bb_;L3@5H{X!5@@^7`c6Hk;w@K{b%`AJ^1~^vb%W z;Cm=HM)$#|H-WO>yzr4e%T;jfx2G#!xGR+QY}cfk{mt#vR(wCy8C0wS{=c4$Jak`q za}OwQ+kE76j6IzAq>e0EtM?!+9`}!Q>i@^yy*)K?{(k^}??2HQ@c9knlx|ZtCQV)n$L2FwT74a(n10>;)XE(u&HV$$QL#i0h5g$v?j&s5Bky<=d->X{4~El-Z?%tIr@T05mTSIgY@>pM#%iV|E}2VVvNJyJ-5Er?=^qpOlXuJ=DnDFUYftwh!Czc zZq0B3g zaYfdTV6sw6{DZx>+7aSgSayg+)FP+Lx%}U%4__fq-N7meqVZ=zSk;md@66p4KCz zZ&{92I*y6q)edB&`lKONxgQ7aT;-KmwLYzDZlyo*cOg4csS4K7V_>q=jaVbA^_Ot2 z-iVnXM0Bpd^dSR9v2{Y@sT1;Rt^&Ofsji{j)Y9-Y@*};}yZW$E2Oouu>ipL=6M(L{ z3?dalMEBCm4y2V|5Dm&sdB8fuD=&++Lw4_ipt`-S`lL|@&*?C+qtO360N@`>n=ARB zShPl0gGi%9)pI{0M_GvE>XHc&P1gl_TYO@$@{pyNJGbKotdRgolPmkYlc;0%?2{K8 zI#>}~BrE|MZ}E|vkM5eH;eO0(2a6q7&0gFe%D&S2a9)}C+MsB>MuU}5yc?-Y4!fJ9pDDi)>lh2n3qU=vh2fNq*iq_Iuma5 z$^Snge7WR9DCk2e{OvmkIi}kA{v(HM#oVJ$>D=Xju5<&IG-n0gIY{C#HrLA`mEC&` zO6i`bJZUHhcN-CN@i#eGbtd`^sfRaK8G3DUcnx?YO$SS|^#bT~CjZs55g$_cN!e~} zJbFxdgjKjuj~MRWeSx+c=$~i*)waydTanAM>jBJ*DnybbLP%WU{t+R5T4cqs4^O3A z&@sxlsNIOngg? zKxFWeh9)6PF0yE$wF)hz>V?0SWEWjJ{PS&T8&$-(*^fM;A( zk=D%sIP+BB^R-cV@Une|j22lMCSD>Lw_lkFE34?FC!D*H*!~yjBLt{AYGm)7=mI^d zb2%wk#JLJZ&>tGSUIYPX7g?0e#iiccU_4L;G@KLU()~Q*{1W;t|Fk`>b~h9jA*(U+ zJ*44zh)#tRk%l>FphcFZ?&lm5cq){S!u=d@MW7Fe#gTtYcaWAw2ds*6WHshaXfAAh zh*iJ7tTg1=K(Q}G(80^w8+vn z@i|4XYER2c-aLIPEU}Hn|9J-?B}N1N>n+H3Oyjk)|I{Poix`Il-eq27DadqBLXD-& zZAEgMpO=qKv>}eXd?;K($cdB6OsIzei)r{R60F69+DTfOg{F6OyPpOku1sou6&eeV z$%le?y4tK_x=Lq4dUZoMg?bpUn3mi3!?RPbog{3&>W8Mud8d>*S+8c3`dBnrkHW#K zGbMCr1^S^_USU+u6PAI|fu_iS(bb)h7HQN2P3GQpcdBTSz?UFmN|Q#ctBncG20=LY z7eaOy8)*+~FBm;|)cg=3Gt)FHaZOTf9sT3>6wxBi7l_%e3Re3QIap`ng4Nc{)0-R2 zhm6Wbtb<^5;mSqCbx0{f29VOMYbB5(TGLiU^5cX;W2LhT1$Z zI&sqknJYjNBk~0snHikciWYG%Kv7go_mAe&&b#pIc|_g({j5M2rK2~&SCoO#7psLN zZAPBLh9*beh#JwF_JZ148QYG9CP6;<)c`7T?+jnp0^r0C%0##ejLvj)CV%2BU}Kzj zZGt36>r)UBH9tfKB4fotdZ>{9<0h!{%{)C=;VIc^h_tZs!RXExF_F|ci!?avzZNlZ z4((n|?2sNl0kl2g)jQrFoe>1#`XxDM-xV9_7hyRV-C1>#q~1Xv((-?CC;fNpK1A$O z)^tt-H^W7iK(Df`uM-4;zuE$zN?FV~a0HD0%nlJT{YrE^Zt~qw^2JBQ?Lx$s0$OC1 z0=U>3-a>loyvg+F@?%~IwXn_usHvc<&y_`Ba$znc;(Kf;*tPb?A!#iguDnHT3x!O*jDl8~12cuIj-6Y-q25NF}$gVjDQbcJv>@8N#*q3+!)1_9P4qEr`wgIRq zQF67v271N#;?IB$`H@1$I-LVkBF zUyRF6ROg!qkxJQ|B&M@61=*I`=-I;49Dw$QOLu?25q{#=bNZA~S;;v89b;xyLzXn* zMJjKn(+A7($%BxZZ9AjM+ER`fuBwziGHzhG(!G8+iQhG|`CuuF(R-s$2pIS5-*pfX z<-1A_8|gGyaHtyk#+c_tlCaN@3wF%Tc&$Ff(4#u(SUv;cr&CprL09DMK328syX`v) zjmk7wTUrCXW4c}|!CL;@>stKqe&E&E%JLhFZ4oNI|IZ*;M2sBTsX3&2uF$x1PvOzx zS|g2BNb&YE=pVC4>vv*z+5--eNMS@;oQ1Ehn&Dn4vAVMOCMrJ2>F_1RsXn0D_Mrx z{YQ`^7_M(ZGa}NGA5r~u!b~rctn&PmJCjxBl_7U9FS@l-LmdBlps-pbF+(d|2;sJt zR2I(w30<$FU}&wzaoiWfrz&G{uvH29`6g6mqX5B{A*6lepX<--v{7%=0g^ZmVJ8u zV{ealYHHHj=UN-<{4j-F#02}br$fozxiHE3#v~$s6+%RZ{h8(Un+coK;U4YdkfRvy zuv^p~wO-FZUvv`jYthw566oyjK<;A9uB1VWMoV&Z+ljatL<}r1`jOih@26)X(H?v@ znJdTiAS)jQ{z!MT^F&d$ivVhEegq z7<0VR_}6;h=$GIPc<;&5`-IrF=UEEbEI_4;UsT zbI;(CEs$xp9oDtZFJA?877_S`Jb6PwC96cf%ae#(u}CDw5`7n|2W!vjI{EX z)P+?B=wCOqdWtkEtnN~(qZ9Pc`~K}Y(j`zbsIE0Nf;cAJSQ$ck43u`L8xZ_A5G{GIxYVs$64 z6*q|9EE=Q7_#1t}Y^CCNLZag{k&Wj_U14TeJqY0z(NB@ZZ+l2pZj*{>YsW33$uW^1 zZ;+za26azQM~VKtHbf>?k&@g7r7H!|y_u+paWah*7sr)tU$y9OJ zm?7PpMY;C+F+3WmRPllV5Is0Nk!y>;{=k#Q>adCvE(eij(~xT(r-p&DVx?o)kchrV z<;KArJZR}q2^#7_qw_!) zYNcbx&lK(bC%5L8;YwAPN>W#kctlH!a?4!;-`cB^)z%;$(ea7gGTy(0qxwo^q8y2c zb_}@{S=ob=@?w>&s~d@kme=G?^ndpop{}U(QQ0NvI=F6FGP~1r`w`i+d`7QL1m$!5Jcv_8loG!K%$mxrEy)xA`YT;qvwbh z2r4UC0gyEY4!X#PKZsWmRu-y3WMSyN#G6`mea5N)k(Hm4;MFnWm1iqc;T8~CSzVI^ zn|rRR4XU^ZL{<)uBtdim@v4fInT~dl10J$;FAfo>zKJoGXY|34L_0%>lT)lr^bdfX zFv-$Imk_5hQ<-RJ1d*ljG0~V$qxH(W`}f_6<3fkc&|j&N1#3WLZO0IC_-E+r?odgZ zVS{KZBI5AR(DYtmDQpmZ{~&1v$?R`qq%vpGByXzKm!++dvtsxR*a1iXj+Q7uw0|3uf)=i1!(G zp5Jr2m}9v{n}%45>_XF+#pK;J`elfHDhNF_q2Nz#U=2H*l*Cy z&p^}N!9EtH9%SkEpCiT}WP_Ak8XproYww`xY-8u0+&MoJTOW@R6R74Z@8mflbsibs z->b-gJ))s~(Ktm8H5q(@lw9#`h&^NT4`Ol))U{v$wupS2;$Z3mF}_lMx0hV`W{89T zMi7(V%kKw}8@q-$h^|9(bFZSe5B7-OM8wJdQ^fT2E0vcIdqhi%;$&?JnoyX9EONHI zD2}$z5z|-4dILF|SrbQ#U!W<^R#mV^bP*M2FOCs2(BOThL##JAcS&2aYkjaqG(9ga zoE;*jzR4*eC7PTPjk{TNPQw6f5lv2r8%N`asjtq3Eu!%;apeOvovk2pT3FtHvi$Y1 zb;ZtI;I^)90T^B$d&dw4VRF(w#jW2dhNC-+GFS?glB``2T?Q9TJEHvG6zollfOcM^ zc@SU)KUSw8fc+m!9|W-YW7*%bz~wz?B0!8E%l?)H6p9+zvjCP-Izhd*0MS&+8kxT* zKs2?wMt9K{AdIC7@~L@@F-j^(xh5$tI! z=4C^Ga1*1k)E5GT^E4i_>j)5PM`OyD1PF6D*0`Po2y^^X;~I1Y2r(RJT#Xw6LbO{N z+w5Gv0|Z7l*bI^a)uqO_b0~nFRAErDCf@?~Gy(G<-vT_N0d^t|fb*hBG)4kgijM{z z9SiWaeoqsbKML@*N?8*zo+H4Qnyx0(Y76kCJ71IW|1ZFo?u`b~RRJqrUo@$CRe&#? zPc)IZ;aH5Gf&JPB`_XssC=55w3ScJ%9aOH#XMk)@Q!o?x4AAXq8q=YC0$9v7 z4R=dE0gQZ2<=PRz&fPtz(-z=^1qu6vc{10>beTcaaYzEvbD){saBar&#Rux znuBxM^aifIzp8Wnqw5GD;`E#M)48U%9PIvRzY6dV=X|ku&~h~~)5zy0)&8rz18lUn z9*$Z|H5!+jpLv_5V*$R9ZC7uvukuUZXf$r|V*h!&aO}wU0Ozz&y}5sxw(730Mn7ZJ zckB6!v3I*&`sG!C$X?IewdQE={`z1x9dugx;ve#@&R{wl?|9EQ+qL%jp(F%2huQOC zzuhjBN~J=f-5s7E|Hi((B&!KTVH`hfLJ*5`L{pYtkqcPJne_QhyX%bp)9+}UU*HB~LH+~osV^rwmdB=9TX?krcM(taos2@z4hvc$ z6wPupd4jKH)Sewl3n|NiX=f$~i3m*@74tIws;Gi|+%*D`2+5j)ZkVw|!V@CGUvW93 zG%(;(;>w$LO=g{Mp3wRTnqi3~R8(A>RcFuBy8lR}Vd;(+(N?Ia6p{%r&Da*&0}IR^i+FGU6a{M@834VW~i`khuq@m!QR4^cz}cwr%|fdp~z4WgW)70oM%U7zS06 zdWWgAj2bD-=Pqw_1~9G61>ygYs>CwIMmCSp+Fw2_9H%&O77xqLkJ@G{Ghten&;Lfd z3Ybp%4hdgDSN{4}JG=mOt?Z~;swHWChjRsk@5)GNY4kZ^(N<6sUI z7TUnH(fdd^42nWvx=1&K1EEa{OcwC9FE@u)5FLl91O`4m=2&6RaN1Jz%=zO@QPM#YQ8QiYZKXS08DRTKJ~XIemX97 zLF~G58UkYg$7)tY@*Ef8N6#8ctBuUD$s_Gy-8gRl*F~%5Uj4{R-V~nw3 z{bb)RloQvZ0F0kd+@(D3wWhB50`yrJ-Ko&xHE4_VJs7={P51*{VaKnW-`L?y#y>1F zP97g8r`D%k`=y&6%Njd+dYCWFWsNnSPN%l4Q|hBtOC24hCa+O3#`k}vI)hHC*7^{m z-hGRlbaKVV!@8^pmuIulp{!9VT6XZ@-nJ!h^Vq}Zgu)WIe9hs@z1p(Wa-bL%rB+=v zFp61!SATD^;FiYz(t7ThU9Td>v(!jIs<^-nS6De0vL20 zfLK}<`W^F9sfFkkkl63{&tpI;>+P&3V9>ZY9F{sj!}gYQZ`perc3UGC+*b+I-+)I; zRw>nLLgvMr`Z;K}ys1zGcW>-f^Xxz(=`E8nXx@2^g=@g?m*UH3;G`|9l$wWtJ?rcE z22{Q2^aK)V@qD?3Uf-Jz9JmgP=iY&>u=wtP#-*%M>Z%Bxu0KBjA+NFF0CsGD`MCHP z+XMdeci9}I(*7JR!r)BSC{?JI%hm9wur3_7OX52R9z?{dl*{G5ze{Hz6NFW9V0I&G zl(N2BeyKhBUv;0uT0gMefk;$5A0uc!$PXzs-UXh26xOO|7ba8jymVkIEdtpNU3=hnB4qASi=r<`ry*QQ$S-X!AVo2aV4KA zb!>9k5Z1>IkUBP}?TzNMe4^B}VRG3J)`J7hbAJwR0q#~~C*cCOSF%p2%D~TMV_4Y; zFexlgl+uXL>)9R5l_5i^>=rQkvN^1A2sn8WU(1D9)|;|1;8m1bby+gGYzXV12UP3g zy8}v=H(8IslPI<7zT@YzF|1?}C?+NF1a2iLt_eGJQEDlE4HV&^w|RhI66J%8K>s-o0_wciBiSym-e z=Ap0S44nAYbu&QiN|r7h*h+d+^ctEiQEH)`0cP1~pkRr}xCRu5Q)}Fwm{aOkyFlk4 z91Yka$ppANdD~8xUC87mpp;btIEuu43IB58LlfxC%A*{>zIZMMfD>CN9tV#UlG@!( zxIn{}fKv4x*372nXF6=d^1M~WWmT{1f7}_Ft|;~Mu%sJfAvKgAhGkWb2pm-`QEDYD zcf}$sY1evq2BUruOsnax2rfC>_mdvpeSWjEx)@5$`wx{B4?|mgH!pXgM(BNbC4qEh zO)I=OZvW}&&K#!HMpoUDe=VidJRJXt8}Z%5JyBbXo4?sRm$l@TFO643Q4WHjj{f7G z_POt%uWO6&0fGXlaJUv`J}6)iK|l_I_}xcl-Cuawp;#g1cIUCT+5OIpMGjTJTD5A` zs^rK)`vw!WG!HMCTuN;FQcw+Nu)TeMorzjRvYEl@AX5t0n2naf@Gto{bz7sxj6S-( z>i#29{`=Ht?CE}glpYQf<*R!})DWJO*6vYFEGo+X(_mk!pUF^Qw?-&cGz&F@*6*GJ zyA`@by=;|!W*^Y2c`4r`+fx4k_an&vW?rx*5_jAA?N~KK3u>tKVADvGz;Wn*b$GC$ zcs21ti{jDWkC>BU^Q~GuXtpx_hCr%R#>s2Er~SHXE%)}kZ(lKQ`;4e#UNW8+i*Dc0 ztaJ30ZovH=chD{A_76B!uQ~lAP1z`hxFx+=*s85l^Z22k5seTm zv5KDAJ)Q0=#{Mc`wcPehWi%XV(7N~6<-6fCG) ziMat5q{qv+vRuLB);;Ai%MXI}ECmbF=Xq~|^{|3vTwrpYL~X6x{kwSw5e5mjp=US7 z_JPW`HEu7YU_n}I-xaW+@erWO6n^8|QY$~*0}Jv!c?m2u4$P25vAEg3+ouSJ%(~t} z(>PysZmT2K5-O(}7wb~6AZ_;3C9nW0NN|?F%Bj>u&26wW-;IaBx^Lq6`%flSD#`6^ z)63XAT-=&^tQG(dtlBR3xSet(IarW7Ra^v%Yv=b&s>M*VwRdF~!GfwW?|_xR#_{M4 zlPj^R(--QyQ}6vKU^-FN1%qw^g1>4h9+ZOxg>Kvgi|cKJ$&~GJTLtp2gQaJ_6f8}( zlP(2IS8b$i`Pk3v?x;O8xjMQ{QDw$WHO21#*foz17*yM{EJZhP5ZY+3ib28BbZsqt z!(mSh z!Sk-SI0S>^EWlEsV;&hH355OJ8moBbDd-Qrqz^fCd_%y}E7^{v>E#;3P~?+fy_KMc z^6UJXua%TVhNl`zj(^LBZ)1_pe2rFj@_DeL&m~Lc;o6pK?XLt&5@NCk3mLvL`K@un z=VwwKtys8BQQzI#K?A+6s&s7dpM>oI{ zW}e-?H^72f(J&j3Yq;vYCnUH`+78pr=6bSOhU+v~FiW==psgSUD>Z~05_AJBret&c z1Ce26xOS^I_%o)kw6Cs##qu!w8E=5KG%b+YG~05%U{a09podx|-g_`4v}gSsw&}>W zJ}XkN*yA;(q^^L)jJCG&$R8Yk9OtIy4WZhS7`Sd-14~K6AlJMA7DDOK^0nxEONY)8 zfs?fGQ*WaO;w_K#9qmemFiF7@=LjQLz_O-$1LmN9cSZ9SeNQXMyHBox#U69;biDwU zvYNRygXV?w4wEW!jK&Zb8>#oMbTpHX(z+?Sbl^dKVsMp##Rf2LE`i0SnmjT*g=va0 zK`GeZqI+P$b%#j1m5%2vU%cuZhG zEAO~{UX+7XQq5B)>n?${sEPp*8!dr7xfzV?@rG+)Aty;6c>%0`7sJp^6fT2QZP5^6 zqDa;oVcPgbYR#EhpIuDQX>!d_4%W+f1y;%>uwYcRjx@|iuaIibtJUg!Z{IzzR=$Il z@d8+xH4Yfyr%bBsRH$wB1Ro`X!*Gir-&7TOD{I2wy6*!%b|-|{lz~;W9649OQl5IU zctgL3OMU@ZW$z4y4=Tnl%#biQb5yO$5lj`*BQ73u;=HmV41E9#KNE!iH1B0#DSKA9 z+Xb*-kAzNQ@S^RaOOK_Y<{iQ8cfnn#EDT>=>rLhy0QTm$QNP7)zJ1r`>M;vQJi8EJ6X z;mFIoA?zQo7iXgEihu5anq$ zXhsOB!zm{slQX#G@ZH-0Wb1Ie@oBKM`E1WpHKcJe5_$=&lRJ)li7y0;pAm(EaRTbZdx{zdaq+Cm?P z-X~*NHFv@4O$|?jrH{^DduUg-ti#cZ&s+6h4#cCCMJi< z(?~lS2tsNC(y-vYKY=$4i>`q+9)uOJIbzbs3~CfHnHyBD7b)LXt)O&T*sjm!VzZLp@?LDDo*gi>GYmqKsE4xWSk zl4=F@p~V~R(}GcG84h4YLU7;R1#84jO(nLO292%H3e|-Sxc;u}HQb`-!Kx>@gXC$X zud8ByA65i~hVEV!$%{jn(c1N%gt7+5&H=3dI>#X0a22cp`NDy%1BkRBLCaR)m#Pix zAz-^jE#I+n=6$db;&hNSjTGBko2zu%A2o5YM=PJ&%mEUD%^|T1KH32mj*a2Iy9w6) zyXgN=VH!eL=3&at<_pQ}F)Z%$_BvRFF?SO7K*(H=KwHHQb#a* zx4>GS%iltd-Qwh{EHMPueu!_dARoF&4XM-WWw6TH&aRpu&Z@0YkL2z;IF5$2@iutX z&n!jaWY?1b7IA8N6PLhRysETg*j5N^(PD#_guZRZ&{z4~FHCaKZO-f72Mcz4my2E? z&ZezidVH-mT_mxc{~h`0x7;Fc)~+WBSV2r=XLVP=@(;%RL;hsHOFpZ(DE7!J;+J#! zbVeaWGs3+LmfyEKiy;`2c4N$V+lhc6@l@&?`=4R?(G z+}=&gA9JW%EtVz^?e{TZ#a533SiZ_yv$v4W*BZ!z!5wu?|6%3f z@$q4BxMni6`H11eji)QTOgaOf&Y_C(_W-L+KdwRkS!9kf&N+0~pk~S5<{4>jNgb=g zt|vKIjV5*9E8IyV0W5p@!c3w@b>l|6UF){BwjxxVw)u2poql7{>tHRa;%tb|-}KQPw^y|x2Md>k`|cK4xh#{aALX`s_lph&z$em+%=2I&N=&A}bhRfW z>B%LP`sjl9o?uij_Dyt zEM@4NEK-zyx(LhuuxwJnH1Y=D)jajKHukTqzZoOW%v zLMwP5tlK;1?SuU;%}}9A{WzoXb+om^%5J^nL;1Ems zrqw`K)%5Xo{Iqn-sGiN{I*qaaLn-yuUSMaFZID6+*)2_qV^8!a3P1`ny)}oEy)Iq7E{IT1?f~GT5h7-_IAa*~!U0y|aJ(_rLrHTWb+~dgyc}H?jX= z-wrSk2!kk&zwZu(iu6`Nq&`us7HNe#RtOmARsj&9iv)xP2p?t{yx$e(f#LrxJeT|@ zdB^1lzC==UsdzrYmB@8oakkn*#8cgpTxW>5YDi9xh)QmEGkYb3@HDf>2PF=BZYBsy zx>$NJObFrKRi;J8?E>OX!^*XuQa#YHe(ap#H&or*$M^XsS}WDvoU^oOQlOOZqDreO zTJtE0s7fILm=e93{{g@|L#%rwVLbKK{1U;DeSv-kXMa=VvC#LlRDsTC5{QsdIn5EGhoFS#`*;B3{)hnCz#OZAL> zanr&PsNcM#%iTCBF=I(XZL15Nv&}Zx@ivP_P~+m(;35p~!c8@~`Ig|%Kz(t~uC0A* ztuY4c>Ae@3S;Y@>8Bujv*#l7pDwUPxlcOcc^k@M+xk6J(^;5PO=_96_ywxD&uhoW>6X_O1&44hlL%#NYnC(w-Qb#+)8ABMD~@HHL>$+ zd1bH>9$lZqv0fvJTbfoH+^{h{<8UnS@5L1=M>i+5ftJk!kv|={*EG{B_$3;h8*W!U z9#>PJ+shyOfP=PTZ(|_1q@y42XnbZ z%d%{aIOJr1K>Vq$ik*2?ZEA*l=vP&{TNMB6P*;|;M>?jeoFTU0QK>$0u$E~N;5s>_ z3b}r?u&t^eaH*)K-U&AGQ>AKukAbFERqFs2ybB@+Z@HTmH8kp{EsDEZUKrm3*4rl> zD^N;NKJ)466N@GFz8_dGEtaY`V)v27Qt1cQYpcbQ$^5v3Hc_Qnz$a}*YpUbb z^{sC@SU=W%>O!yvVW)pe3mvPBYI24buJq4#ykGdt!C9}dt@cCpffdy=whrpU4!IN1 z9H`Ol_`*7>4ct)JI}b1QlT9`J0|5oL7rp;7Jzhxfv|(~1IrW@dBB@-l;bGyiu#s?(V5u|H+kqu{K{j5i zcESsiG_YL%VO}8#!TPFI9{B_j0j+~e*;{k`cICPE+%P}rJ+GpU1Zn{sHRry zONST}S7*&ls`^V*>gM2#O7!w@yN=aKeT-m-7)roOw{lz_bAIwOGkD0VATh|2cAtR7 zfp}^J7KAshOafNwJ(ioBn|v>ix0_N-+94*3f`z^bbTQT+tec1Ee^+kb75SeD&AM}T z5*P*R6pwK?!#GcgVLWu1Pie4S<$4|sQy30NFT(e5+h-bzX>&!2RJP zi&073Ly2b&z1G>$W`vi*@oAI~h^8 z2Zwr~nsK%*SRD?vX-xIZ?GCJOE_J0v-0i5Bf>rR!2TMts#quiO7lTqcN|^*Kt}F_c z2yf3{S%t}>V6l6=-6~PHIK@)Qp?$FpSc=wbfxO}J@4@1n$fsela-1TEJ1JO^QL=)G z2Oiq6nFJXl8}^D}2MU(my_aAC2m^qskkp3_5E+3r=TUzyc!?>SaI=Ke(ADe z2$oRfKeBR}JMd$&V#b34tRu0!pNz&!Ugx3h1d!_2dVCzOaByHP^CJs;2^OTH#>jFn z@4Bk?d3OMoZ9w&OceI1a+67qP_VBrxz3MR|2D>MZ$gv4wRv4{ao4zux&-&DALzj9TPqartf6%wlW zI@LqLW=JkXM#ypmR^&U1eE#6T>iMCn!$W%qmgx8>SnGkof_>h0eFHUFfx%i()v4+2 zz=CJ)i!=HB6->3yViiLmdgu?_ zyguy;;mHREtIMI>)Q#Zw$eDkS5vzi&gcu8n874>!t~lQ;>Ey#TkPo6bAO4cW~#AYzFJNNr4Tb3$eN< z6Wp2(5Uj>w9=0a;5Ue!<7RE3~TR^Y?NdWMZNBwTw1z7OsxYm9L7RC-DuvqV^Stgh) zKd`bmZ;){yU}1P;_fjGF8G-f253C~abUb6Fau1MA6UsehNa!3V1dPg^Z^VovjRn|e5j=No%u#!osHs{;IK`JI2{12b1|F_ z%zgc3V0qnk{yc5_30A9JRXugxOOw)X!5ZSRvsbI__%|?E^gPx6wQF431z6SZG*<*c z=%GJYtT+QcDOecZSPq!10AOKQOV1FOrCnio7_t=o5AU~*1po`I6ikDU{QJOi+SWM< zt-ArMe{NA%XUC4fnix<)%QK(ek4oEv1=nxUufF;lu$mSYQLFU~B=OFZYh~)|w-G5Y zSi)jiU+Y<}OafLlOig%8zygZ};SFG_d=mhy;uOf{LrF@pAy}u(S^$Ey`^{Skeqfnm zj%^4No0v<77dw9;Sgg$f8Hb&Nh3}d{9|D5~YFkk?dD_Q%M-9P3a|Yv6Z=Z@L%NQ&V zUf!4CcQ;^xXg)Al*%qCV95h)1Ea=2CiV0X?v9v*78K*Hl09Zx(i|phiFj+=m!O-oP z?!M1o@{d@4Iz!)6U%vELf;BzE`kP1c)J6LZZi% z=9g*nfx!Zc1-%;Zk@kRqbw761pVXsSsqF|X&zGM_+-62>1cY*~I znpV1YZr;DnqJAv}21{t7xvl#%#I3qUL$KCV%yZUouix+77OaNeS=6@?=2K05N9Po< zWp@V#3rbV(QD_8MTE^i%f`t|fu}Q~1aeJ_CYZGQL?NDUc6~bqXmnRycA_-WgGl!3( zPWX)qWv+C{9v{-q$uqEYz$ua_qGr{M!L}&~IN;gDn zxq8`r>+g(Mka1{eZrN|Jps__4%y2dUYvePBdvt_re6}5?A@3S2(5wSS00F^*4lu7; z6Q?L6or0xh9P$NNV6jpwszGlGA+5eGSYWYu8+yZ!;lhSsod{OUr}@8A0l|W*a?E*{ zH1ppJ)|%M)NWj90P@cK{d8K2J&8)QRfoYy9aBs2j*4;1gor^msV-2 zOaYt-SX##6;R^&y>n$@X_1fiFX#7eCEEb1QubKwaN)5q^nw`gp#fUVtT@$G5!!Yt94M8VY;7nJ)%l^{PZ@vUUp=*u}ab zKd=A*YlZ)_(r6MBy%`r0^RW$6}+sK^j3c;pIf1q2IbZ2fcY z6wZ(#U>!DTA&*u+ulNlpIgdBlev6wZGO=ZNgn-39@{B{@%3gpaeO1-L5w@*9;p{02 z7R;?78Xnm_SjcBxy&qUGB_ZW5L*383eY-@nSVWa?C|J2gJfJ;GL9qBJk?8SOu%OB# z-C_~FDli1=lo^J4hgl%7a6VRK6fD6YV8I3)LLE7M<_tIdiF^oo)`?}#vpm|d{T4@M zX3{qm51uUnYlu4>d;4C1H9du`8w#j4j98 zN8hfq3{IHwUI!0Pz=F0E^NkGvW#wqJ9=YW3 zb4)uP&Jz_uY`?W+SJkF}_02jC5Nr~#oPB!&*2JnqRqd_ha7~}*hg-s`A5_)#Yb#jZ zK9AolEHGT3M|KF7)Ts`4I5%EO>e&nd7OqQurI{#57Ce6hDJHa4V(|m)5wNOX39!IF zWiu4+QLxH5dY0P&ags=ni9I8*AOwYm1O)5Ic?FmB@HD-Anu4XR!nt}x3tmTYajLd} zBJ?m^K{_0YW6Av$4EsETzlNrVW?@O`)F=T9GY&fiYpqeJc^bj$*Y?`hp_PFN=C3Z^ z%QY-EfALB+t$po$yLl^Eqhq)NTBIz%WEp}ryyUxjy-iG!$H9#>j$KJ$HyQK;Ks zvJAmGclug*;uf%ij$9OiLT;^0LAdG}%oYU;Ad;g_CMKTbTp>e{PnF{mjwBvQh~rz( zeeSpBT`;X=aBPr2S$v!ntjV7{1xsCS^64Xj<$xW#*7!;rwLo8?(ACt_-&YIsTRb0} z6fA7k(@7_V_HF}Kdy`LN^^C2mAp4L0z5$*9gLT*xtmJ1FuF{t;%i%jy(oxG~BnV|~ z2J1m3f&~@}!y8V`0+R(`aetr6Rn8IQuqEpj3un5EwL5nW!D6<^XlXN8a&VkahZ&^1 z#Ovn-n3P4qa)!%X!B?*a!}l4l@An)`u~%bFoI3L%w*MOKQGJ>kqF{*$#DT#I{A)9= zPogqXQYU^g1XSl{6fDfywj+;t1WgtJ%kbA`TE|H9Fi(Sk#mdqwLhl}EOo(Ztn5P2g zMZuD8BUsh%NO*&u)mou9;-@Mt2b)QMWd_plw~fIg^w<%>9ui~-7^4A)8TyCkqysD>_P0M^jwJps$rwuJgQcjUrzlwLcqs4#AjOH;bn%*K;@T1RN7ON_>Kc46Au`{|qUBD{ zPQY6EEm*F)IfI2>t=}CR$H(4#4Zs=~c{bDnWU>TUzqSDjQ~dQ`1?|h-(~N=zPC!FxgI52NDQA5vrYn0?v)xw~1y0`J_->?&GhvA1oibcA~#fDIzCL*H8l(m%nvc2#`|Vw^!Qc( zy=P`k^6x%>_Vje<^!4}jbcynhGvw<1dUG``pQf5t9LvKW{4k-bNuVd0uaVM{ zJB5X!0nkHUrOXd3XIMl;0s-rARK#@+p7?*Fhz_w^{PVD=n0P%Z3jBV^(Wrj}G1AHK z^TEOAub=R5^HRdKxX`$3376O(lXtY$-8$+O`;x7u#wPi8Yieq|`o_eBAW7S+5w*M% zR8Fj+2~OWe-s$?r#u4hiBsgERDy~QiyrL4wHbLbTNz&$}mevtbk$?qgy(ues`xLj6 zlapanC|56#o|1BjmrC#nUqV;_t)QObQPMCe5MW8sk&!k6*170t-Nk#OshsTG1bq6Q z6)*Ja^}K%PUpR5%f|C(RiSZ%vi4yyN?Uf9cfBjbo!TR5J>3|3T2nYb!oeYZmcBD}P zV9T&;A=YHHS|X=oV#PVJ000000000000000000000H6byXLtnAumuMI0000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5415c57d9ae8bf774c235bd0b1b5c7eb4908850b Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:52:24 +0100 Subject: [PATCH 3/9] Fix team scoping bug and add object picker to matches/group-standings - Add name_en to indexed properties so context.objects[0].name_en is accessible in scripts (was undefined, causing all team filters to silently return no results) - Update team-standing, next-match, last-match scripts to use name_en - Convert matches and group-standings to httpRequestScopedSingle with the same World Cup Team object picker as team-standing Co-Authored-By: Claude Sonnet 4.6 --- .../v1/dataStreams/scripts/group-standings.js | 5 ++--- .../v1/dataStreams/scripts/last-match.js | 2 +- .../v1/dataStreams/scripts/matches.js | 14 ++++---------- .../v1/dataStreams/scripts/next-match.js | 2 +- .../v1/dataStreams/scripts/team-standing.js | 2 +- .../dataStreams/worldcup2026-group-standings.json | 15 ++++++--------- .../v1/dataStreams/worldcup2026-matches.json | 15 ++++++--------- .../WorldCup2026/v1/indexDefinitions/default.json | 1 + plugins/WorldCup2026/v1/metadata.json | 2 +- 9 files changed, 23 insertions(+), 35 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index b67c465f..2bb405e7 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamFilter = context.config.team || ''; +var teamFilter = context.objects[0] ? context.objects[0].name_en : ''; var groupGames = games.filter(function(g) { return g.type === 'group'; }); @@ -56,9 +56,8 @@ var rows = Object.keys(standings).map(function(k) { }); if (teamFilter) { - var filterLower = teamFilter.toLowerCase(); var matchedTeam = rows.filter(function(r) { - return r.team.toLowerCase().indexOf(filterLower) !== -1; + return r.team === teamFilter; })[0]; if (matchedTeam) { var targetGroup = matchedTeam.group; diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js index f0cb83b8..0ddd8cc4 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name : ''; +var teamName = context.objects[0] ? context.objects[0].name_en : ''; var stageMap = { group: 'Group Stage', diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js index c1496400..da5f0c23 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -1,5 +1,5 @@ var games = data.games || []; -var team = context.config.team || ''; +var teamName = context.objects[0] ? context.objects[0].name_en : ''; var stageMap = { group: 'Group Stage', @@ -24,15 +24,9 @@ function getTeamName(game, side) { return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); } -var filtered = games; -if (team) { - var teamLower = team.toLowerCase(); - filtered = games.filter(function(g) { - var home = (g.home_team_name_en || '').toLowerCase(); - var away = (g.away_team_name_en || '').toLowerCase(); - return home.indexOf(teamLower) !== -1 || away.indexOf(teamLower) !== -1; - }); -} +var filtered = games.filter(function(g) { + return g.home_team_name_en === teamName || g.away_team_name_en === teamName; +}); filtered.sort(function(a, b) { return parseDate(a.local_date) - parseDate(b.local_date); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js index 6b254b2d..a822ab18 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name : ''; +var teamName = context.objects[0] ? context.objects[0].name_en : ''; var stageMap = { group: 'Group Stage', diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index 7537d35c..4f02a043 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name : ''; +var teamName = context.objects[0] ? context.objects[0].name_en : ''; var groupGames = games.filter(function(g) { return g.type === 'group' && diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json index b1020b4f..1ed08569 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json @@ -3,21 +3,18 @@ "displayName": "Group Standings", "description": "Group stage standings calculated from match results, with optional filter to a specific team's group", "tags": ["Standings"], - "baseDataSourceName": "httpRequestUnscoped", + "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", "endpointPath": "/get/games", "postRequestScript": "group-standings.js" }, - "ui": [ - { - "label": "Filter by Team (leave blank for all groups)", - "name": "team", - "type": "text", - "defaultValue": "" + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" } - ], - "matches": "none", + }, "timeframes": false, "metadata": [ { "name": "group", "displayName": "Group", "shape": "string" }, diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json index 6a217b83..226bf031 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json @@ -3,21 +3,18 @@ "displayName": "Matches", "description": "All 104 World Cup 2026 fixtures in date order, with optional filter by team", "tags": ["Matches"], - "baseDataSourceName": "httpRequestUnscoped", + "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", "endpointPath": "/get/games", "postRequestScript": "matches.js" }, - "ui": [ - { - "label": "Filter by Team (leave blank for all)", - "name": "team", - "type": "text", - "defaultValue": "" + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" } - ], - "matches": "none", + }, "timeframes": false, "metadata": [ { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, diff --git a/plugins/WorldCup2026/v1/indexDefinitions/default.json b/plugins/WorldCup2026/v1/indexDefinitions/default.json index 4fb8b961..0f0af0a3 100644 --- a/plugins/WorldCup2026/v1/indexDefinitions/default.json +++ b/plugins/WorldCup2026/v1/indexDefinitions/default.json @@ -11,6 +11,7 @@ "name": "name_en", "type": { "value": "World Cup Team" }, "properties": [ + "name_en", "fifa_code", "flag", { "group": "groups" }, diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index a60e828c..1031a4a0 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "@TimWheeler-SQUP", "type": "community" From ce2a5899d5566f380c5d4f275bb06e3037977727 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:20:12 +0100 Subject: [PATCH 4/9] Fix team scoping bug and add object picker to matches/group-standings - Switch scoped streams from name-based to ID-based team matching (name_en property key may be normalised by SquaredUp; teamId is unambiguous and more robust) - Store teamName and teamId as explicit properties with mapped keys - Fix isHome check in team-standing to use team ID not name - Add Country column to Team Standing data stream - Rename MP -> Played and Pts -> Points in Group Standings Co-Authored-By: Claude Sonnet 4.6 --- .../v1/dataStreams/scripts/group-standings.js | 2 +- .../WorldCup2026/v1/dataStreams/scripts/last-match.js | 4 ++-- plugins/WorldCup2026/v1/dataStreams/scripts/matches.js | 4 ++-- .../WorldCup2026/v1/dataStreams/scripts/next-match.js | 4 ++-- .../WorldCup2026/v1/dataStreams/scripts/team-standing.js | 9 +++++---- .../v1/dataStreams/worldcup2026-group-standings.json | 4 ++-- .../v1/dataStreams/worldcup2026-team-standing.json | 1 + plugins/WorldCup2026/v1/indexDefinitions/default.json | 3 ++- plugins/WorldCup2026/v1/metadata.json | 2 +- 9 files changed, 18 insertions(+), 15 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index 2bb405e7..028356ca 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamFilter = context.objects[0] ? context.objects[0].name_en : ''; +var teamFilter = context.objects[0] ? context.objects[0].teamName : ''; var groupGames = games.filter(function(g) { return g.type === 'group'; }); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js index 0ddd8cc4..9ca88df2 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name_en : ''; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; var stageMap = { group: 'Group Stage', @@ -19,7 +19,7 @@ function parseDate(d) { var played = games.filter(function(g) { return g.finished === 'TRUE' && - (g.home_team_name_en === teamName || g.away_team_name_en === teamName); + (g.home_team_id === teamId || g.away_team_id === teamId); }); played.sort(function(a, b) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js index da5f0c23..98e1adf1 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name_en : ''; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; var stageMap = { group: 'Group Stage', @@ -25,7 +25,7 @@ function getTeamName(game, side) { } var filtered = games.filter(function(g) { - return g.home_team_name_en === teamName || g.away_team_name_en === teamName; + return g.home_team_id === teamId || g.away_team_id === teamId; }); filtered.sort(function(a, b) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js index a822ab18..b1c1bc2d 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name_en : ''; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; var stageMap = { group: 'Group Stage', @@ -18,7 +18,7 @@ function parseDate(d) { } var teamGames = games.filter(function(g) { - return g.home_team_name_en === teamName || g.away_team_name_en === teamName; + return g.home_team_id === teamId || g.away_team_id === teamId; }); var upcoming = teamGames.filter(function(g) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index 4f02a043..603c193a 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -1,9 +1,10 @@ var games = data.games || []; -var teamName = context.objects[0] ? context.objects[0].name_en : ''; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; +var teamName = context.objects[0] ? context.objects[0].teamName : ''; var groupGames = games.filter(function(g) { return g.type === 'group' && - (g.home_team_name_en === teamName || g.away_team_name_en === teamName); + (g.home_team_id === teamId || g.away_team_id === teamId); }); var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0; @@ -11,7 +12,7 @@ var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0; groupGames.forEach(function(g) { if (g.finished !== 'TRUE') return; mp++; - var isHome = g.home_team_name_en === teamName; + var isHome = g.home_team_id === teamId; var myScore = parseInt(isHome ? g.home_score : g.away_score, 10) || 0; var oppScore = parseInt(isHome ? g.away_score : g.home_score, 10) || 0; gf += myScore; @@ -24,4 +25,4 @@ groupGames.forEach(function(g) { var pts = (w * 3) + d; var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; -result = [{ mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, sourceId: sourceId }]; +result = [{ country: teamName, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, sourceId: sourceId }]; diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json index 1ed08569..192eeb26 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json @@ -19,13 +19,13 @@ "metadata": [ { "name": "group", "displayName": "Group", "shape": "string" }, { "name": "team", "displayName": "Team", "shape": "string", "role": "label" }, - { "name": "mp", "displayName": "MP", "shape": "number" }, + { "name": "mp", "displayName": "Played", "shape": "number" }, { "name": "w", "displayName": "W", "shape": "number" }, { "name": "d", "displayName": "D", "shape": "number" }, { "name": "l", "displayName": "L", "shape": "number" }, { "name": "gf", "displayName": "GF", "shape": "number" }, { "name": "ga", "displayName": "GA", "shape": "number" }, { "name": "gd", "displayName": "GD", "shape": "number" }, - { "name": "pts", "displayName": "Pts", "shape": "number" } + { "name": "pts", "displayName": "Points", "shape": "number" } ] } diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json index 2ca61154..c6371a14 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json @@ -17,6 +17,7 @@ }, "timeframes": false, "metadata": [ + { "name": "country", "displayName": "Country", "shape": "string", "role": "label" }, { "name": "mp", "displayName": "Played", "shape": "number" }, { "name": "w", "displayName": "Won", "shape": "number" }, { "name": "d", "displayName": "Drawn", "shape": "number" }, diff --git a/plugins/WorldCup2026/v1/indexDefinitions/default.json b/plugins/WorldCup2026/v1/indexDefinitions/default.json index 0f0af0a3..4a8b877a 100644 --- a/plugins/WorldCup2026/v1/indexDefinitions/default.json +++ b/plugins/WorldCup2026/v1/indexDefinitions/default.json @@ -11,7 +11,8 @@ "name": "name_en", "type": { "value": "World Cup Team" }, "properties": [ - "name_en", + { "teamName": "name_en" }, + { "teamId": "id" }, "fifa_code", "flag", { "group": "groups" }, diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index 1031a4a0..21cfc3b3 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "@TimWheeler-SQUP", "type": "community" From e18dfbb64ab92c2f8d18687de7da8c7f7c6934dc Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:35:40 +0100 Subject: [PATCH 5/9] Fix teamName undefined error and array unwrapping in scripts - Fix isHome check in next-match and last-match to use teamId (teamName was removed but still referenced, causing script errors) - Unwrap teamName property with [].concat()[0] since SquaredUp stores indexed properties as arrays Co-Authored-By: Claude Sonnet 4.6 --- plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js | 2 +- plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js | 2 +- plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js | 2 +- plugins/WorldCup2026/v1/metadata.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js index 9ca88df2..3ea951ee 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -32,7 +32,7 @@ if (played.length === 0) { result = [{ date: 'No matches played yet', home_away: '', opponent: '', score: '', result: '', stage: '', sourceId: sourceId }]; } else { var last = played[0]; - var isHome = last.home_team_name_en === teamName; + var isHome = last.home_team_id === teamId; var opponent = isHome ? last.away_team_name_en : last.home_team_name_en; var myScore = parseInt(isHome ? last.home_score : last.away_score, 10); var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js index b1c1bc2d..5e77c140 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -35,7 +35,7 @@ if (upcoming.length === 0) { result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }]; } else { var next = upcoming[0]; - var isHome = next.home_team_name_en === teamName; + var isHome = next.home_team_id === teamId; var opponent = isHome ? next.away_team_name_en : next.home_team_name_en; if (!opponent || opponent === 'undefined') { opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD'); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index 603c193a..b3bdca00 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -1,6 +1,6 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; -var teamName = context.objects[0] ? context.objects[0].teamName : ''; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; var groupGames = games.filter(function(g) { return g.type === 'group' && diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index 21cfc3b3..b5624456 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.0.2", + "version": "1.0.3", "author": { "name": "@TimWheeler-SQUP", "type": "community" From 43639abfb05c58e7dbdef5b6a9722f710d84ec20 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:45:24 +0100 Subject: [PATCH 6/9] Fix Group Standings filter when scoped to a team object Indexed properties are stored as arrays in SquaredUp, so teamFilter was ["Mexico"] instead of "Mexico", causing the strict equality check to never match and returning all 48 teams. Apply [].concat()[0] unwrapping consistent with other scoped scripts. Co-Authored-By: Claude Sonnet 4.6 --- plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js | 2 +- plugins/WorldCup2026/v1/metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index 028356ca..6f511e98 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -1,5 +1,5 @@ var games = data.games || []; -var teamFilter = context.objects[0] ? context.objects[0].teamName : ''; +var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; var groupGames = games.filter(function(g) { return g.type === 'group'; }); diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index b5624456..b444f1ef 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.0.3", + "version": "1.0.4", "author": { "name": "@TimWheeler-SQUP", "type": "community" From 7b0873deeaf856ebebc165ff8616e11f7b90ff10 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:09:15 +0100 Subject: [PATCH 7/9] Add flag emojis, group/goal-diff columns, and Group Standings OOB tile - Flag emojis prepended to all team/opponent name columns across group-standings, matches, next-match, last-match (name lookup), and team-standing (iso2-based conversion with ENG/SCO special cases) - team-standing stream: add group and goal difference columns - Stats OOB tile: now shows Country, Group, GF, GA, GD, Points - Group Points OOB tile replaced with scoped Group Standings table - teams-import: make id column hidden by default Co-Authored-By: Claude Sonnet 4.6 --- .../v1/dataStreams/scripts/group-standings.js | 46 +++++++++++-------- .../v1/dataStreams/scripts/last-match.js | 19 +++++++- .../v1/dataStreams/scripts/matches.js | 29 +++++++++++- .../v1/dataStreams/scripts/next-match.js | 19 +++++++- .../v1/dataStreams/scripts/team-standing.js | 22 ++++++++- .../worldcup2026-team-standing.json | 4 +- .../worldcup2026-teams-import.json | 2 +- .../v1/defaultContent/team.dash.json | 18 ++++---- plugins/WorldCup2026/v1/metadata.json | 2 +- 9 files changed, 124 insertions(+), 37 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index 6f511e98..1a9217e5 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -1,13 +1,29 @@ var games = data.games || []; var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var FLAG = { + 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', + 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', + 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', + 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', + 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', + 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', + 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', + 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', + 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', + 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', + 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', + 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', + 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' +}; + var groupGames = games.filter(function(g) { return g.type === 'group'; }); var standings = {}; function ensureTeam(name, group) { if (!standings[name]) { - standings[name] = { team: name, group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, gd: 0, pts: 0 }; + standings[name] = { group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 }; } } @@ -22,27 +38,24 @@ groupGames.forEach(function(g) { var home = standings[g.home_team_name_en]; var away = standings[g.away_team_name_en]; - home.mp++; - away.mp++; - home.gf += homeScore; - home.ga += awayScore; - away.gf += awayScore; - away.ga += homeScore; + home.mp++; away.mp++; + home.gf += homeScore; home.ga += awayScore; + away.gf += awayScore; away.ga += homeScore; if (homeScore > awayScore) { home.w++; home.pts += 3; away.l++; } else if (awayScore > homeScore) { away.w++; away.pts += 3; home.l++; } else { - home.d++; home.pts += 1; away.d++; away.pts += 1; + home.d++; home.pts++; away.d++; away.pts++; } }); var rows = Object.keys(standings).map(function(k) { var s = standings[k]; - var gd = s.gf - s.ga; + var flag = FLAG[k] || ''; return { - team: s.team, + team: flag ? flag + ' ' + k : k, group: s.group, mp: s.mp, w: s.w, @@ -50,19 +63,14 @@ var rows = Object.keys(standings).map(function(k) { l: s.l, gf: s.gf, ga: s.ga, - gd: gd, + gd: s.gf - s.ga, pts: s.pts }; }); -if (teamFilter) { - var matchedTeam = rows.filter(function(r) { - return r.team === teamFilter; - })[0]; - if (matchedTeam) { - var targetGroup = matchedTeam.group; - rows = rows.filter(function(r) { return r.group === targetGroup; }); - } +if (teamFilter && standings[teamFilter]) { + var targetGroup = standings[teamFilter].group; + rows = rows.filter(function(r) { return r.group === targetGroup; }); } rows.sort(function(a, b) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js index 3ea951ee..24290b0f 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -1,6 +1,22 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; +var FLAG = { + 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', + 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', + 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', + 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', + 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', + 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', + 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', + 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', + 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', + 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', + 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', + 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', + 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' +}; + var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -38,11 +54,12 @@ if (played.length === 0) { var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10); var scoreStr = isHome ? last.home_score + '-' + last.away_score : last.away_score + '-' + last.home_score; var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw'; + var opFlag = FLAG[opponent] || ''; result = [{ date: last.local_date, home_away: isHome ? 'Home' : 'Away', - opponent: opponent, + opponent: opFlag ? opFlag + ' ' + opponent : opponent, score: scoreStr, result: matchResult, stage: stageMap[last.type] || last.type, diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js index 98e1adf1..8bafd42a 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -1,6 +1,22 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; +var FLAG = { + 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', + 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', + 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', + 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', + 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', + 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', + 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', + 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', + 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', + 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', + 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', + 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', + 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' +}; + var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -17,11 +33,20 @@ function parseDate(d) { return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); } +function withFlag(name) { + if (!name) return name; + var flag = FLAG[name]; + return flag ? flag + ' ' + name : name; +} + function getTeamName(game, side) { + var name; if (side === 'home') { - return game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); + name = game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); + } else { + name = game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); } - return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); + return withFlag(name); } var filtered = games.filter(function(g) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js index 5e77c140..5359e5b5 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -1,6 +1,22 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; +var FLAG = { + 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', + 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', + 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', + 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', + 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', + 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', + 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', + 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', + 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', + 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', + 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', + 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', + 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' +}; + var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -40,10 +56,11 @@ if (upcoming.length === 0) { if (!opponent || opponent === 'undefined') { opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD'); } + var opFlag = FLAG[opponent] || ''; result = [{ date: next.local_date, home_away: isHome ? 'Home' : 'Away', - opponent: opponent, + opponent: opFlag ? opFlag + ' ' + opponent : opponent, stage: stageMap[next.type] || next.type, group: next.type === 'group' ? 'Group ' + next.group : '', sourceId: sourceId diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index b3bdca00..2bd992f7 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -1,6 +1,23 @@ var games = data.games || []; -var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; +var teamId = context.objects[0] ? String([].concat(context.objects[0].teamId)[0] || '') : ''; var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var iso2Raw = context.objects[0] ? [].concat(context.objects[0].iso2)[0] || '' : ''; +var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : ''; + +function flagFromIso2(code) { + if (!code) return ''; + if (code === 'ENG') return '🏴󠁧󠁢󠁥󠁮󠁧󠁿'; + if (code === 'SCO') return '🏴󠁧󠁢󠁳󠁣󠁴󠁿'; + if (code.length === 2) { + return code.toUpperCase().split('').map(function(c) { + return String.fromCodePoint(c.charCodeAt(0) + 127397); + }).join(''); + } + return ''; +} + +var flag = flagFromIso2(iso2Raw); +var country = flag ? flag + ' ' + teamName : teamName; var groupGames = games.filter(function(g) { return g.type === 'group' && @@ -23,6 +40,7 @@ groupGames.forEach(function(g) { }); var pts = (w * 3) + d; +var gd = gf - ga; var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; -result = [{ country: teamName, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, sourceId: sourceId }]; +result = [{ country: country, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: sourceId }]; diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json index c6371a14..1241cf33 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json @@ -18,13 +18,15 @@ "timeframes": false, "metadata": [ { "name": "country", "displayName": "Country", "shape": "string", "role": "label" }, + { "name": "group", "displayName": "Group", "shape": "string" }, { "name": "mp", "displayName": "Played", "shape": "number" }, { "name": "w", "displayName": "Won", "shape": "number" }, { "name": "d", "displayName": "Drawn", "shape": "number" }, { "name": "l", "displayName": "Lost", "shape": "number" }, - { "name": "pts", "displayName": "Points", "shape": "number" }, { "name": "gf", "displayName": "Goals For", "shape": "number" }, { "name": "ga", "displayName": "Goals Against", "shape": "number" }, + { "name": "gd", "displayName": "Goal Difference", "shape": "number" }, + { "name": "pts", "displayName": "Points", "shape": "number" }, { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } ] } diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json index e841e863..c1627e54 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json @@ -13,7 +13,7 @@ "timeframes": false, "visibility": { "type": "hidden" }, "metadata": [ - { "name": "id", "shape": "string" }, + { "name": "id", "shape": "string", "visible": false }, { "name": "name_en", "displayName": "Team", "shape": "string", "role": "label" }, { "name": "fifa_code", "displayName": "FIFA Code", "shape": "string" }, { "name": "flag", "displayName": "Flag", "shape": "url" }, diff --git a/plugins/WorldCup2026/v1/defaultContent/team.dash.json b/plugins/WorldCup2026/v1/defaultContent/team.dash.json index 4b7ee5d3..15ab1ca0 100644 --- a/plugins/WorldCup2026/v1/defaultContent/team.dash.json +++ b/plugins/WorldCup2026/v1/defaultContent/team.dash.json @@ -94,14 +94,14 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Group Points", + "title": "Group Standings", "description": "", "timeframe": "none", "variables": ["{{variables.[Team]}}"], "activePluginConfigIds": ["{{configId}}"], "dataStream": { - "id": "{{dataStreams.worldcup2026-team-standing}}", - "name": "worldcup2026-team-standing", + "id": "{{dataStreams.worldcup2026-group-standings}}", + "name": "worldcup2026-group-standings", "pluginConfigId": "{{configId}}" }, "scope": { @@ -113,9 +113,9 @@ "type": "data-stream-table", "config": { "data-stream-table": { - "transpose": true, - "columnOrder": ["pts", "mp", "w", "d", "l"], - "hiddenColumns": ["gf", "ga", "gd"] + "transpose": false, + "columnOrder": ["team", "mp", "w", "d", "l", "gf", "ga", "gd", "pts"], + "hiddenColumns": ["group"] } } } @@ -133,7 +133,7 @@ "config": { "_type": "tile/data-stream", "title": "Stats", - "description": "Total goals for, against and difference", + "description": "", "timeframe": "none", "variables": ["{{variables.[Team]}}"], "activePluginConfigIds": ["{{configId}}"], @@ -152,8 +152,8 @@ "config": { "data-stream-table": { "transpose": true, - "columnOrder": ["gf", "ga", "gd"], - "hiddenColumns": ["pts", "mp", "w", "d", "l"] + "columnOrder": ["country", "group", "gf", "ga", "gd", "pts"], + "hiddenColumns": ["mp", "w", "d", "l", "sourceId"] } } } diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index b444f1ef..a9875274 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.0.4", + "version": "1.1.0", "author": { "name": "@TimWheeler-SQUP", "type": "community" From e559fdbe2d729d7ae67e4bcd01408aee67beded4 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:55:55 +0100 Subject: [PATCH 8/9] Fix team scoping bug and add object picker to matches/group-standings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert flag emojis — Regional Indicator Symbol pairs render as letter codes (HR, GH, PA) rather than flag images in SquaredUp's table tile. Co-Authored-By: Claude Sonnet 4.6 --- .../v1/dataStreams/scripts/group-standings.js | 19 +----------- .../v1/dataStreams/scripts/last-match.js | 19 +----------- .../v1/dataStreams/scripts/matches.js | 29 ++----------------- .../v1/dataStreams/scripts/next-match.js | 19 +----------- .../v1/dataStreams/scripts/team-standing.js | 18 +----------- plugins/WorldCup2026/v1/metadata.json | 2 +- 6 files changed, 7 insertions(+), 99 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index 1a9217e5..aa75cc14 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -1,22 +1,6 @@ var games = data.games || []; var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; -var FLAG = { - 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', - 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', - 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', - 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', - 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', - 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', - 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', - 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', - 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', - 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', - 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', - 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', - 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' -}; - var groupGames = games.filter(function(g) { return g.type === 'group'; }); var standings = {}; @@ -53,9 +37,8 @@ groupGames.forEach(function(g) { var rows = Object.keys(standings).map(function(k) { var s = standings[k]; - var flag = FLAG[k] || ''; return { - team: flag ? flag + ' ' + k : k, + team: k, group: s.group, mp: s.mp, w: s.w, diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js index 24290b0f..3ea951ee 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -1,22 +1,6 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; -var FLAG = { - 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', - 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', - 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', - 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', - 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', - 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', - 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', - 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', - 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', - 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', - 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', - 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', - 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' -}; - var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -54,12 +38,11 @@ if (played.length === 0) { var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10); var scoreStr = isHome ? last.home_score + '-' + last.away_score : last.away_score + '-' + last.home_score; var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw'; - var opFlag = FLAG[opponent] || ''; result = [{ date: last.local_date, home_away: isHome ? 'Home' : 'Away', - opponent: opFlag ? opFlag + ' ' + opponent : opponent, + opponent: opponent, score: scoreStr, result: matchResult, stage: stageMap[last.type] || last.type, diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js index 8bafd42a..98e1adf1 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -1,22 +1,6 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; -var FLAG = { - 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', - 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', - 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', - 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', - 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', - 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', - 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', - 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', - 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', - 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', - 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', - 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', - 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' -}; - var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -33,20 +17,11 @@ function parseDate(d) { return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); } -function withFlag(name) { - if (!name) return name; - var flag = FLAG[name]; - return flag ? flag + ' ' + name : name; -} - function getTeamName(game, side) { - var name; if (side === 'home') { - name = game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); - } else { - name = game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); + return game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); } - return withFlag(name); + return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); } var filtered = games.filter(function(g) { diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js index 5359e5b5..5e77c140 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -1,22 +1,6 @@ var games = data.games || []; var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; -var FLAG = { - 'Algeria': '🇩🇿', 'Argentina': '🇦🇷', 'Australia': '🇦🇺', 'Austria': '🇦🇹', - 'Belgium': '🇧🇪', 'Bosnia and Herzegovina': '🇧🇦', 'Brazil': '🇧🇷', - 'Canada': '🇨🇦', 'Cape Verde': '🇨🇻', 'Colombia': '🇨🇴', 'Croatia': '🇭🇷', - 'Curaçao': '🇨🇼', 'Czech Republic': '🇨🇿', 'Democratic Republic of the Congo': '🇨🇩', - 'Ecuador': '🇪🇨', 'Egypt': '🇪🇬', 'England': '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'France': '🇫🇷', - 'Germany': '🇩🇪', 'Ghana': '🇬🇭', 'Haiti': '🇭🇹', 'Iran': '🇮🇷', 'Iraq': '🇮🇶', - 'Ivory Coast': '🇨🇮', 'Japan': '🇯🇵', 'Jordan': '🇯🇴', 'Mexico': '🇲🇽', - 'Morocco': '🇲🇦', 'Netherlands': '🇳🇱', 'New Zealand': '🇳🇿', 'Norway': '🇳🇴', - 'Panama': '🇵🇦', 'Paraguay': '🇵🇾', 'Portugal': '🇵🇹', 'Qatar': '🇶🇦', - 'Saudi Arabia': '🇸🇦', 'Scotland': '🏴󠁧󠁢󠁳󠁣󠁴󠁿', 'Senegal': '🇸🇳', - 'South Africa': '🇿🇦', 'South Korea': '🇰🇷', 'Spain': '🇪🇸', 'Sweden': '🇸🇪', - 'Switzerland': '🇨🇭', 'Tunisia': '🇹🇳', 'Turkey': '🇹🇷', - 'United States': '🇺🇸', 'Uruguay': '🇺🇾', 'Uzbekistan': '🇺🇿' -}; - var stageMap = { group: 'Group Stage', r32: 'Round of 32', @@ -56,11 +40,10 @@ if (upcoming.length === 0) { if (!opponent || opponent === 'undefined') { opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD'); } - var opFlag = FLAG[opponent] || ''; result = [{ date: next.local_date, home_away: isHome ? 'Home' : 'Away', - opponent: opFlag ? opFlag + ' ' + opponent : opponent, + opponent: opponent, stage: stageMap[next.type] || next.type, group: next.type === 'group' ? 'Group ' + next.group : '', sourceId: sourceId diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index 2bd992f7..aa0f6a94 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -1,24 +1,8 @@ var games = data.games || []; var teamId = context.objects[0] ? String([].concat(context.objects[0].teamId)[0] || '') : ''; var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; -var iso2Raw = context.objects[0] ? [].concat(context.objects[0].iso2)[0] || '' : ''; var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : ''; -function flagFromIso2(code) { - if (!code) return ''; - if (code === 'ENG') return '🏴󠁧󠁢󠁥󠁮󠁧󠁿'; - if (code === 'SCO') return '🏴󠁧󠁢󠁳󠁣󠁴󠁿'; - if (code.length === 2) { - return code.toUpperCase().split('').map(function(c) { - return String.fromCodePoint(c.charCodeAt(0) + 127397); - }).join(''); - } - return ''; -} - -var flag = flagFromIso2(iso2Raw); -var country = flag ? flag + ' ' + teamName : teamName; - var groupGames = games.filter(function(g) { return g.type === 'group' && (g.home_team_id === teamId || g.away_team_id === teamId); @@ -43,4 +27,4 @@ var pts = (w * 3) + d; var gd = gf - ga; var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; -result = [{ country: country, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: sourceId }]; +result = [{ country: teamName, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: sourceId }]; diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index a9875274..10939c03 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.1.0", + "version": "1.1.1", "author": { "name": "@TimWheeler-SQUP", "type": "community" From 7ba5221283fb8abb113f9d025145c56a8f3e06f3 Mon Sep 17 00:00:00 2001 From: Tim Wheeler <63284593+TimWheeler-SQUP@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:08:14 +0100 Subject: [PATCH 9/9] Add drilldown links to Country and Team name columns Clicking Country in Team Standing or Team in Group Standings now navigates to the World Cup Team object. Raw team ID is output as the sourceId column; SquaredUp resolves World Cup Team~{id} in the graph via the drilldown metadata entry on each stream. Co-Authored-By: Claude Sonnet 4.6 --- .../v1/dataStreams/scripts/group-standings.js | 9 +++++---- .../WorldCup2026/v1/dataStreams/scripts/team-standing.js | 3 +-- .../v1/dataStreams/worldcup2026-group-standings.json | 4 +++- .../v1/dataStreams/worldcup2026-team-standing.json | 3 ++- plugins/WorldCup2026/v1/metadata.json | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js index aa75cc14..0caa592e 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -5,15 +5,15 @@ var groupGames = games.filter(function(g) { return g.type === 'group'; }); var standings = {}; -function ensureTeam(name, group) { +function ensureTeam(name, id, group) { if (!standings[name]) { - standings[name] = { group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 }; + standings[name] = { id: id, group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 }; } } groupGames.forEach(function(g) { - ensureTeam(g.home_team_name_en, g.group); - ensureTeam(g.away_team_name_en, g.group); + ensureTeam(g.home_team_name_en, g.home_team_id, g.group); + ensureTeam(g.away_team_name_en, g.away_team_id, g.group); if (g.finished !== 'TRUE') return; @@ -38,6 +38,7 @@ groupGames.forEach(function(g) { var rows = Object.keys(standings).map(function(k) { var s = standings[k]; return { + sourceId: s.id, team: k, group: s.group, mp: s.mp, diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js index aa0f6a94..2711046c 100644 --- a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -26,5 +26,4 @@ groupGames.forEach(function(g) { var pts = (w * 3) + d; var gd = gf - ga; -var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; -result = [{ country: teamName, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: sourceId }]; +result = [{ country: teamName, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: teamId }]; diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json index 192eeb26..79a815af 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json @@ -17,6 +17,7 @@ }, "timeframes": false, "metadata": [ + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false }, { "name": "group", "displayName": "Group", "shape": "string" }, { "name": "team", "displayName": "Team", "shape": "string", "role": "label" }, { "name": "mp", "displayName": "Played", "shape": "number" }, @@ -26,6 +27,7 @@ { "name": "gf", "displayName": "GF", "shape": "number" }, { "name": "ga", "displayName": "GA", "shape": "number" }, { "name": "gd", "displayName": "GD", "shape": "number" }, - { "name": "pts", "displayName": "Points", "shape": "number" } + { "name": "pts", "displayName": "Points", "shape": "number" }, + { "name": "team", "sourceId": "sourceId", "sourceType": "World Cup Team" } ] } diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json index 1241cf33..506c3891 100644 --- a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json @@ -27,6 +27,7 @@ { "name": "ga", "displayName": "Goals Against", "shape": "number" }, { "name": "gd", "displayName": "Goal Difference", "shape": "number" }, { "name": "pts", "displayName": "Points", "shape": "number" }, - { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false }, + { "name": "country", "sourceId": "sourceId", "sourceType": "World Cup Team" } ] } diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json index 10939c03..13d352d3 100644 --- a/plugins/WorldCup2026/v1/metadata.json +++ b/plugins/WorldCup2026/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "worldcup2026", "displayName": "FIFA World Cup 2026", - "version": "1.1.1", + "version": "1.1.2", "author": { "name": "@TimWheeler-SQUP", "type": "community"