feat(widgets): add live tennis widget - #6442
Conversation
Adds a Live Tennis widget showing live, upcoming or completed matches from the ATP, WTA, Challenger, ITF and junior tours via livetennisapi.com. The API key is read server side from LIVE_TENNIS_API_KEY and is never exposed to the browser, since board item options are readable by anyone who can view the board. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🚨 Preview Deployment Blocked - Security ProtectionYour pull request was blocked from triggering preview deployments Why was this blocked?
How to resolve this:Option 1: Get Collaborator Access (Recommended) Option 2: Request Permission Override For Repository Administrators:To disable this security check ( This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments. 🛡️ Learn more about this security featureThis protection prevents unauthorized users from:
Preview deployments are powerful but require trust. Only users with repository write access can trigger them. |
📝 WalkthroughWalkthroughAdds a Live Tennis dashboard widget with configurable tour, status, match count, tournament, and ranking options. It fetches and validates server-side match data, maps API-key failures to an unauthorized state, renders live and scheduled matches, and adds translations and documentation. ChangesLive Tennis widget
Sequence Diagram(s)sequenceDiagram
participant User
participant TennisWidget
participant tennisRouter
participant fetchTennisMatchesHandler
participant LiveTennisAPI
User->>TennisWidget: Configure tennis options
TennisWidget->>tennisRouter: Request matches
tennisRouter->>fetchTennisMatchesHandler: Execute validated query
fetchTennisMatchesHandler->>LiveTennisAPI: Fetch filtered matches
LiveTennisAPI-->>fetchTennisMatchesHandler: Return match data
fetchTennisMatchesHandler-->>tennisRouter: Return mapped matches
tennisRouter-->>TennisWidget: Return matches or UNAUTHORIZED
TennisWidget-->>User: Render match or error state
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/docs/docs/widgets/tennis/index.mdx`:
- Line 30: Update the tennis widget documentation text to hyphenate the compound
modifier as “server-side” in both occurrences, including the additional
referenced occurrence, while leaving the surrounding wording unchanged.
In `@apps/docs/docs/widgets/tennis/index.tsx`:
- Around line 7-8: Update the tennis widget metadata and tour-filter
configuration to remove Juniors, including the description near the widget
definition and the corresponding filter option. Keep ATP, WTA, Challenger, and
ITF available.
In `@packages/api/src/router/widgets/tennis.ts`:
- Line 18: Add MCP metadata to the getMatches procedure before tennisInputSchema
is applied via .input(), enabling discovery with mcp.enabled set to true and
providing a clear user-facing description.
- Around line 9-15: Require integer values for matchCount in the
tennisInputSchema at packages/api/src/router/widgets/tennis.ts:9-15 by adding
integer validation alongside the existing range checks. Apply the same integer
validation to the matchCount schema in
packages/widgets/src/tennis/index.ts:29-34 so saved widget options follow the
API contract.
In `@packages/widgets/src/tennis/component.tsx`:
- Around line 72-74: Update the tennis widget query in the component around
useQuery to destructure and inspect error, handling an UNAUTHORIZED result
before the !result fallback and rendering the existing translated API-key
configuration state documented for this widget. Preserve WidgetEmptyState for
non-unauthorized missing data.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 06b96e14-d377-4b1a-8b70-5f0c565f6986
📒 Files selected for processing (16)
apps/docs/docs/advanced/environment-variables/index.mdxapps/docs/docs/widgets/tennis/index.mdxapps/docs/docs/widgets/tennis/index.tsxpackages/api/src/router/widgets/index.tspackages/api/src/router/widgets/tennis.tspackages/definitions/src/docs/widget-doc-slugs.tspackages/definitions/src/widget.tspackages/request-handler/src/env.tspackages/request-handler/src/tennis-mapping.spec.tspackages/request-handler/src/tennis-mapping.tspackages/request-handler/src/tennis.tspackages/request-handler/src/test/tennis.spec.tspackages/translation/src/lang/en.jsonpackages/widgets/src/index.tsxpackages/widgets/src/tennis/component.tsxpackages/widgets/src/tennis/index.ts
| This widget uses the [Live Tennis API](https://livetennisapi.com), which requires an API key. | ||
| A free tier is available without a credit card at https://livetennisapi.com/subscribe/free. | ||
|
|
||
| The key is read **server side only** from the `LIVE_TENNIS_API_KEY` environment variable and is |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Hyphenate the compound modifier.
Use server-side in both locations.
Proposed fix
- The key is read **server side only**
+ The key is read **server-side only**
- Match data is fetched server side from
+ Match data is fetched server-side fromAlso applies to: 50-50
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/docs/docs/widgets/tennis/index.mdx` at line 30, Update the tennis widget
documentation text to hyphenate the compound modifier as “server-side” in both
occurrences, including the additional referenced occurrence, while leaving the
surrounding wording unchanged.
Source: Linters/SAST tools
| description: | ||
| "Displays live, upcoming or completed tennis matches from the ATP, WTA, Challenger, ITF and junior tours.", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== tennis widget doc ==\n'
sed -n '1,120p' apps/docs/docs/widgets/tennis/index.tsx
printf '\n== search livetennisapi references ==\n'
rg -n "livetennisapi|ATP, WTA, Challenger, ITF|junior" apps docs packages -g '!**/node_modules/**' || trueRepository: homarr-labs/homarr
Length of output: 3449
🌐 Web query:
livetennisapi free tier supports ATP WTA Challenger ITF juniors documentation
💡 Result:
The Live Tennis API supports ATP, WTA, Challenger, and ITF tours [1][2][3]. While the API documentation references these tours, it does not explicitly differentiate junior-level coverage as a distinct category from its general ITF support [1][3]. Regarding the free tier, the Live Tennis API offers a Basic tier that provides access to the following core functionalities: - Matches, scores, and match details [4][3] - Player searches and player profiles [4][3] - Fixtures (upcoming matches) [4][3] - Completed match results (history) [4][5][3] Features such as match events, market prices, model analysis, win probability, and WebSocket access are reserved for higher-paid tiers (Pro and Ultra) and are not included in the Basic/Free tier [4][1][3].
Citations:
- 1: https://github.com/livetennisapi/livetennisapi-python
- 2: https://github.com/livetennisapi/livetennisapi-js
- 3: https://github.com/livetennisapi/livetennisapi-mcp
- 4: https://pypi.org/project/livetennisapi/1.0.2/
- 5: https://pypi.org/project/livetennisapi/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== widgets tennis implementation ==\n'
sed -n '1,220p' packages/widgets/src/tennis/index.ts
printf '\n== request handler tennis implementation ==\n'
sed -n '1,260p' packages/request-handler/src/tennis.ts
printf '\n== search juniors usage ==\n'
rg -n "\bjuniors\b|tennisTours|tour" packages/widgets/src/tennis packages/request-handler/src/tennis.ts packages/request-handler/src -g '!**/node_modules/**' || trueRepository: homarr-labs/homarr
Length of output: 6158
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== outline packages/widgets/src/tennis/index.ts ==\n'
ast-grep outline packages/widgets/src/tennis/index.ts --view expanded || true
printf '\n== outline packages/request-handler/src/tennis.ts ==\n'
ast-grep outline packages/request-handler/src/tennis.ts --view expanded || trueRepository: homarr-labs/homarr
Length of output: 784
🌐 Web query:
site:livetennisapi.com tour juniors Live Tennis API matches tour parameter juniors
💡 Result:
I couldn’t find any tour=juniors filter for GET /matches. The documented query params are only status, limit, and offset. The API says it covers ATP, WTA, Challenger, and ITF, and the reference does not mention “juniors” anywhere. (docs.livetennisapi.com)
If you need juniors, it may be outside this API’s supported tour list.
Citations:
Remove Juniors from the tennis widget
The API only exposes ATP, WTA, Challenger, and ITF tours, so Juniors shouldn’t be listed in the description or offered as a tour filter.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/docs/docs/widgets/tennis/index.tsx` around lines 7 - 8, Update the
tennis widget metadata and tour-filter configuration to remove Juniors,
including the description near the widget definition and the corresponding
filter option. Keep ATP, WTA, Challenger, and ITF available.
Source: Coding guidelines
The widget destructured only `data` from the query, so a missing or rejected LIVE_TENNIS_API_KEY fell through to the generic empty state. The router does raise UNAUTHORIZED and the definition does declare a dedicated error entry, but nothing connected the two: react-query defaults `throwOnError` to false and this repository sets no global override, so the error never reached WidgetError. The documented "No valid Live Tennis API key configured" message was therefore unreachable. Escalate only that error to the boundary, so every other failure keeps the inline empty state, and skip the query client's three default retries for it — a rejected key is not transient, and retrying only delayed the message by three backoff rounds while the board showed nothing. Also addresses review feedback: `matchCount` is now constrained to an integer in both the router schema and the slider validator, and `getMatches` declares MCP metadata like the other widget queries. Adds a render test over WidgetError. The existing unit tests only proved the predicate and the definition entry; they would have passed against the original bug. The render test fails if the error entry is removed, in which case the widget claims a permissions problem rather than a configuration one.
|
Thanks @coderabbitai — one of these caught a real error in my own PR description, so I want to correct that explicitly rather than quietly fix it. Correction: the API key error state did not workMy PR body said a missing or rejected key shows a dedicated configuration message. That was not true. The router raised I verified the router's behaviour and reported it as the rendered result. That was my mistake and the review was right to flag it. Fixed in I also added a render test over Also addressed
Two I'd push back on, with evidenceJuniors is not a dead option. I probed the API before removing it. "server side" is correct unhyphenated here. Both uses are adverbial — "read server side only", "fetched server side" — where the unhyphenated form is standard; the hyphen belongs to the attributive use ("a server-side call"). It also matches the existing prose in Note for maintainers: |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Follow-up commit summary ==='
git show --stat --oneline --decorate --no-renames fa6e790
printf '%s\n' '=== Follow-up diff (targeted) ==='
git show --format=fuller --no-ext-diff --unified=80 --no-renames fa6e790 -- \
packages/widgets/src/tennis \
packages/api/src/router/widgets/tennis.ts \
packages/request-handler/src/tennis.ts \
packages/request-handler/src/test \
packages/request-handler/src \
| sed -n '1,1400p'
printf '%s\n' '=== Relevant symbol outlines ==='
ast-grep outline packages/widgets/src/tennis --items all 2>/dev/null || true
ast-grep outline packages/api/src/router/widgets/tennis.ts --items all 2>/dev/null || true
printf '%s\n' '=== Error/retry and schema call sites ==='
rg -n -C 5 --glob '*.{ts,tsx}' \
'WidgetError|throwOnError|retry\s*:|retry\(|isError|matchCount|mcp\s*:' \
packages/widgets/src/tennis packages/api/src/router/widgets/tennis.ts packages/request-handler/src \
|| trueLength of output: 42266 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Widget error boundary and host candidates ==='
fd -t f . packages/widgets/src | rg '/(errors|widget).*\.(ts|tsx)$|/component\.(ts|tsx)$' | sed -n '1,240p'
printf '%s\n' '=== Definitions/usages of WidgetError and error boundary ==='
rg -n -C 8 --glob '*.{ts,tsx}' \
'WidgetError|ErrorBoundary|resetErrorBoundary|throwOnError' \
packages/widgets/src | sed -n '1,1000p'
printf '%s\n' '=== Comparable widget query-error tests ==='
rg -n -C 6 --glob '*.spec.ts' --glob '*.spec.tsx' \
'useQueryMock|TRPCClientError|throwOnError|ErrorBoundary|WidgetError' \
packages/widgets/src | sed -n '1,1200p'Length of output: 44001
I agree with both rebuttals: One test-coverage detail remains: Please add a regression test that mounts ✏️ Learnings added
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/widgets/src/tennis/error-state.spec.ts (1)
54-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWrap test cleanup in
act.root.unmount()does React work during teardown, so calling it outsideactcan trigger React 19 warnings. Resetroot/containertoundefinedafter cleanup if you want to avoid stale handles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/widgets/src/tennis/error-state.spec.ts` around lines 54 - 57, Update the afterEach cleanup around root.unmount in the test suite to execute the unmount within React’s act helper, then remove the container as before. Reset root and container to undefined after cleanup to prevent stale handles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/widgets/src/tennis/error-state.spec.ts`:
- Around line 54-57: Update the afterEach cleanup around root.unmount in the
test suite to execute the unmount within React’s act helper, then remove the
container as before. Reset root and container to undefined after cleanup to
prevent stale handles.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c3527c79-db61-4547-acf4-a04fda5c772e
📒 Files selected for processing (6)
packages/api/src/router/widgets/tennis.tspackages/widgets/src/tennis/api-key-error.spec.tspackages/widgets/src/tennis/api-key-error.tspackages/widgets/src/tennis/component.tsxpackages/widgets/src/tennis/error-state.spec.tspackages/widgets/src/tennis/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/widgets/src/tennis/index.ts
- packages/widgets/src/tennis/component.tsx
|
Yeah @bensynapse this is too much of a claude PR with a super small range of users who will be interested by this since it's so outside of the scope of what homarr usually does. I would be much happier if you spent you Claude credits on helping me with the v2 branch #6356 in order to bring the custom widgets feature to a state where it can support that live tennis widget you did. I genuinely think that this could be great, but not as a "core" widget, as a custom one. On v2 there is a widget store where you can share custom widgets you make, they can all have options, requests, actions, ... If you could instead try to prompt Claude to do "Checkout v2, understand the PR and the changes it brings and setup the homarr mcp, then iterate with it until you can make an exact replica of #6442 inside of the custom widgets feature. If not possible, describe all the necessary steps and constraints we must overcome to make it so" This would help me and the community much more, because then other people than you will be able to make use of the new custom widgets directly in one-shot without Claude code ;) |
|
@ajnart took you up on this — checked out `v2` and worked through the custom-widget path (schema, the source/request executor, the MCP + skill). Short version: it's not just possible, it's a better home for this than core was — and specifically it fixes the thing that actually sank the core version. The blocker on #6442 was the API key: on a public board a key in a widget option is readable by anyone through the public `getBoardByName`, which is why I was pushed into a server-side `LIVE_TENNIS_API_KEY` env var. The v2 `apiKeyHeader` source auth solves that natively — the user enters their key at install, it's stored encrypted and injected server-side, and the JSX template never sees it. The options (tour/status/count/switches) and the 401 "no key configured" branch map straight onto custom-widget primitives. Honest tradeoffs, since they're the reason I'd argued for core: it loses compile-time types, the unit tests, and i18n (options are single-label). And the per-set score + serving-dot derivation that lived in a server-side TS mapper has to be re-expressed in the JSX expression sandbox — that's the one non-trivial piece. Happy to author it as a manifest and publish to Workshop so it's the one-shot install you described. Do you want me to go at it against `v2` as it stands now, or wait until the branch settles a bit — the docs still flag the format as likely to change, and I'd rather not ship something that breaks under you. |
These are not relevant in that context, it's not that deep. I think you should continue iterating on v2's capabilities until you are convinced that the outcome widget is close to what you initially wanted. Then you can pinpoint implement flaws that would counter against it. For that you have to actually use the custom widget and play around with it to spot its flaws, instead of letting Claude answer your GitHub replies |
Adds a Live Tennis widget showing live, upcoming or completed matches from the ATP, WTA, Challenger, ITF and junior tours. Homarr currently has no sports widget of any kind.
Why a dedicated widget rather than
customApicustomApigenuinely can render this, and I don't want to pretend otherwise: it fetches server-side with encrypted credentials, and its JSX template whitelistsTable/Badge/Groupwith.map()and ternaries viaSAFE_BINDINGS. It even whitelistsPaginatedListandTabsContainer, so interactivity is available too.The case for a dedicated widget is domain rendering, not saved configuration effort. The API returns per-set game counts as
games[0]/games[1], which have to be split across two players and aligned withsets, plus aserverindex and current game points. Every user would reconstruct that in a JSX template by hand — unversioned, untranslated, untested. This ships it typed, translated and covered.I'll be straight that this is a moderate justification rather than a slam dunk. If you'd rather it stayed a
customApirecipe in the docs, that's a reasonable call and I'll close this.API key handling
The key is read server-side from
LIVE_TENNIS_API_KEYand is deliberately not a widget option.getBoardByNameinpackages/api/src/router/board.tsis apublicProcedure, and it returns item options unredacted:So a credential stored in a widget option would be readable by anonymous visitors on a public board. That also matches the existing code: no widget stores a credential in an option today, and
widgetSecretsis scoped to one kind (if (!item || item.kind !== "releases") throw). If you'd prefer this go throughwidgetSecretsinstead, I'm happy to rework it.Free tier, no card. Only free-tier endpoints are used —
/analysisand/eventsreturn 403 on the free plan, so nothing is built against them and they aren't advertised.Manual smoke test
Verified against the live API for live, upcoming and completed matches and per-tour filtering, including doubles (null country) and upcoming matches (null score).
Offline and error paths: a missing key or a 401 renders "No valid Live Tennis API key configured" with a retry button; 5xx and DNS failure hit the standard widget error boundary;
NO_EXTERNAL_CONNECTIONshort-circuits without making a request. 13 unit tests cover the mapping and every error branch.Checks
pnpm buildpasses (4/4 tasks).pnpm lintshows no new findings — per-package warning counts are byte-identical todev, and no lint output mentions tennis.One heads-up:
@homarr/dblint is already failing ondev(no-shadow, 5 errors), anddbdepends ondefinitions, which any new widget must touch — so--affectedmay surface that here through no fault of this PR. I reproduced it on a pristine checkout ofcf0e469to confirm.Docs
In-tree at
apps/docs/docs/widgets/tennis/. The PR template still points athomarr-labs/documentation, but that repo is archived, so I followedAGENTS.mdand kept it to one PR. English-only translations; no other locales touched. No temp files.AI assistance
This PR was written with AI assistance (Claude). All build, lint, test and live-API output was executed and verified rather than assumed, and both the
customApitrade-off and thepublicProcedurecredential finding were checked against the source before being claimed here.Summary by CodeRabbit
LIVE_TENNIS_API_KEY.