Skip to content

feat(widgets): add live tennis widget - #6442

Open
bensynapse wants to merge 2 commits into
homarr-labs:devfrom
bensynapse:feat/tennis-widget
Open

feat(widgets): add live tennis widget#6442
bensynapse wants to merge 2 commits into
homarr-labs:devfrom
bensynapse:feat/tennis-widget

Conversation

@bensynapse

@bensynapse bensynapse commented Jul 22, 2026

Copy link
Copy Markdown

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 customApi

customApi genuinely can render this, and I don't want to pretend otherwise: it fetches server-side with encrypted credentials, and its JSX template whitelists Table/Badge/Group with .map() and ternaries via SAFE_BINDINGS. It even whitelists PaginatedList and TabsContainer, 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 with sets, plus a server index 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 customApi recipe 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_KEY and is deliberately not a widget option.

getBoardByName in packages/api/src/router/board.ts is a publicProcedure, and it returns item options unredacted:

options: superjson.parse<Record<string, unknown>>(item.options),

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 widgetSecrets is scoped to one kind (if (!item || item.kind !== "releases") throw). If you'd prefer this go through widgetSecrets instead, I'm happy to rework it.

Free tier, no card. Only free-tier endpoints are used — /analysis and /events return 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_CONNECTION short-circuits without making a request. 13 unit tests cover the mapping and every error branch.

Checks

pnpm build passes (4/4 tasks). pnpm lint shows no new findings — per-package warning counts are byte-identical to dev, and no lint output mentions tennis.

One heads-up: @homarr/db lint is already failing on dev (no-shadow, 5 errors), and db depends on definitions, which any new widget must touch — so --affected may surface that here through no fault of this PR. I reproduced it on a pristine checkout of cf0e469 to confirm.

Docs

In-tree at apps/docs/docs/widgets/tennis/. The PR template still points at homarr-labs/documentation, but that repo is archived, so I followed AGENTS.md and 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 customApi trade-off and the publicProcedure credential finding were checked against the source before being claimed here.

Summary by CodeRabbit

  • New Features
    • Added a Live Tennis dashboard widget with live, upcoming, and completed match views.
    • Added widget configuration for tour, match status, match count (1–20), and optional tournament name and player ranking display.
    • Added a new documentation page and a Widgets environment variable section for LIVE_TENNIS_API_KEY.
  • Bug Fixes
    • Improved API-key error handling with a tennis-specific unauthorized state and no retries for auth failures.
    • Added correct behavior when external connections are disabled, plus robust response parsing and validation.

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>
@bensynapse
bensynapse requested a review from a team as a code owner July 22, 2026 14:52
@dokploy-homarr-labs

Copy link
Copy Markdown

🚨 Preview Deployment Blocked - Security Protection

Your pull request was blocked from triggering preview deployments

Why was this blocked?

  • User: bensynapse
  • Repository: homarr
  • Permission Level: read
  • Required Level: write, maintain, or admin

How to resolve this:

Option 1: Get Collaborator Access (Recommended)
Ask a repository maintainer to invite you as a collaborator with write permissions or higher.

Option 2: Request Permission Override
Ask a repository administrator to disable security validation for this specific application if appropriate.

For Repository Administrators:

To disable this security check (⚠️ not recommended for public repositories):
Enter to preview settings and disable the 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 feature

This protection prevents unauthorized users from:

  • Executing malicious code on the deployment server
  • Accessing environment variables and secrets
  • Potentially compromising the infrastructure

Preview deployments are powerful but require trust. Only users with repository write access can trigger them.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Live Tennis widget

Layer / File(s) Summary
Widget contract and configuration
packages/definitions/src/widget.ts, packages/definitions/src/docs/widget-doc-slugs.ts, packages/widgets/src/tennis/*, packages/widgets/src/index.tsx, packages/api/src/router/widgets/tennis.ts, packages/translation/src/lang/en.json, apps/docs/docs/widgets/tennis/index.tsx
Registers the tennis widget kind, supported options, localized labels, defaults, error configuration, documentation metadata, and validated API input.
API fetch and match mapping
packages/request-handler/src/env.ts, packages/request-handler/src/tennis-mapping.ts, packages/request-handler/src/tennis.ts, packages/request-handler/src/*tennis*.spec.ts
Validates and maps Live Tennis API responses, fetches filtered matches with the server-side API key, handles disabled external connections and authorization failures, and tests these paths.
tRPC router exposure
packages/api/src/router/widgets/index.ts, packages/api/src/router/widgets/tennis.ts
Adds the tennis getMatches query to the widget router and converts API-key errors into UNAUTHORIZED tRPC errors.
Widget rendering and documentation
packages/widgets/src/tennis/component.tsx, packages/widgets/src/tennis/api-key-error.ts, apps/docs/docs/widgets/tennis/index.mdx, apps/docs/docs/advanced/environment-variables/index.mdx
Renders match, player, score, serving, live-status, scheduled-time, empty, and unauthorized states, while documenting setup and LIVE_TENNIS_API_KEY usage.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Live Tennis widget.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Yagni / Over-Engineering ✅ Passed PASS: The widget follows existing patterns, uses only small focused helpers where reused, and adds no new dependencies or broad abstractions.
Docs Are Up To Date ✅ Passed apps/docs includes a new Live Tennis widget page plus the LIVE_TENNIS_API_KEY env-var entry, and the widget docs slug maps to tennis.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cf0e469 and 1a2e9a7.

📒 Files selected for processing (16)
  • apps/docs/docs/advanced/environment-variables/index.mdx
  • apps/docs/docs/widgets/tennis/index.mdx
  • apps/docs/docs/widgets/tennis/index.tsx
  • packages/api/src/router/widgets/index.ts
  • packages/api/src/router/widgets/tennis.ts
  • packages/definitions/src/docs/widget-doc-slugs.ts
  • packages/definitions/src/widget.ts
  • packages/request-handler/src/env.ts
  • packages/request-handler/src/tennis-mapping.spec.ts
  • packages/request-handler/src/tennis-mapping.ts
  • packages/request-handler/src/tennis.ts
  • packages/request-handler/src/test/tennis.spec.ts
  • packages/translation/src/lang/en.json
  • packages/widgets/src/index.tsx
  • packages/widgets/src/tennis/component.tsx
  • packages/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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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 from

Also 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

Comment on lines +7 to +8
description:
"Displays live, upcoming or completed tennis matches from the ATP, WTA, Challenger, ITF and junior tours.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/**' || true

Repository: 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:


🏁 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/**' || true

Repository: 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 || true

Repository: 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

Comment thread packages/api/src/router/widgets/tennis.ts
Comment thread packages/api/src/router/widgets/tennis.ts Outdated
Comment thread packages/widgets/src/tennis/component.tsx Outdated
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.
@bensynapse

Copy link
Copy Markdown
Author

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 work

My PR body said a missing or rejected key shows a dedicated configuration message. That was not true. The router raised UNAUTHORIZED and the definition declared the error entry, but the component destructured only data, and react-query defaults throwOnError to false — this repo sets no global override — so the error never reached WidgetError. Users got the generic empty state, and the message documented in apps/docs/docs/widgets/tennis/index.mdx was unreachable.

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 fa6e790, along with a second issue found while fixing it: the query client sets retry: 3, so a rejected key was retried three times with backoff before showing anything. A bad key is not transient, so that path no longer retries.

I also added a render test over WidgetError. The existing unit tests would have passed against the original bug, which is exactly why they weren't enough. The new test asserts rendered output, and I confirmed it fails when the error entry is removed — in that case the widget claims "You don't have permission to access this widget", which is the wrong diagnosis for a misconfigured key.

Also addressed

  • matchCount now requires an integer, in both the router schema and the slider validator.
  • getMatches now declares mcp metadata, matching patchmon and health-monitoring.

Two I'd push back on, with evidence

Juniors is not a dead option. I probed the API before removing it. ?tour=juniors resolves correctly and returns real matches — the fixtures endpoint currently returns Grand Slam junior events (e.g. French Open Boys). It reads as empty today only because junior draws run at Grand Slams and none is in progress. Removing it would delete a working filter that repopulates several times a year, so I've kept 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 apps/docs/docs/management/settings/index.mdx, and the docs contain no instance of "server-side". Happy to change it if you'd prefer the hyphen for consistency.

Note for maintainers: pnpm lint reports 5 pre-existing errors in @homarr/db (collection.ts, test/schema.spec.ts) that are present on a clean checkout of this branch's base and unrelated to this PR.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

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 \
  || true

Length 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


@bensynapse Thanks for the thorough follow-up. I verified that fa6e790 fixes the actual behavior:

  • TennisWidget now escalates only the API-key UNAUTHORIZED error to the widget boundary and skips retries for it.
  • Other failures retain the existing inline empty-state behavior and default retry policy.
  • matchCount is integral in both validation layers, and getMatches now has MCP metadata.

I agree with both rebuttals: juniors is a valid, seasonally populated API filter, and “server side” is correct for these adverbial uses. I’m withdrawing those points.

One test-coverage detail remains: packages/widgets/src/tennis/error-state.spec.ts directly renders WidgetError with an already-created UNAUTHORIZED error. It verifies that the boundary renders the correct message, but it does not exercise the original broken handoff from TennisWidget’s useQuery to that boundary. That test would have passed before this fix because the widget-specific error definition already existed.

Please add a regression test that mounts TennisWidget under the real widget error boundary and makes getMatches.useQuery reject with UNAUTHORIZED; it should assert the configured-key message is rendered. That would fail if throwOnError were removed and directly protects the bug you found.


✏️ Learnings added
Learnt from: bensynapse
URL: https://github.com/homarr-labs/homarr/pull/6442

Timestamp: 2026-07-22T15:41:13.223Z
Learning: For the Live Tennis widget, the upstream Live Tennis API supports `tour=juniors` and returns junior Grand Slam matches when those seasonal events are active; do not treat the `juniors` filter as a dead option merely because it is temporarily empty.

Learnt from: bensynapse
URL: https://github.com/homarr-labs/homarr/pull/6442

Timestamp: 2026-07-22T15:41:13.223Z
Learning: In Homarr documentation, use unhyphenated “server side” when it is adverbial (for example, “fetched server side”); reserve hyphenated “server-side” for attributive uses.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/widgets/src/tennis/error-state.spec.ts (1)

54-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap test cleanup in act. root.unmount() does React work during teardown, so calling it outside act can trigger React 19 warnings. Reset root/container to undefined after 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1a2e9a7 and fa6e790.

📒 Files selected for processing (6)
  • packages/api/src/router/widgets/tennis.ts
  • packages/widgets/src/tennis/api-key-error.spec.ts
  • packages/widgets/src/tennis/api-key-error.ts
  • packages/widgets/src/tennis/component.tsx
  • packages/widgets/src/tennis/error-state.spec.ts
  • packages/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

@ajnart

ajnart commented Jul 22, 2026

Copy link
Copy Markdown
Member

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 ;)

@bensynapse

Copy link
Copy Markdown
Author

@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.

@ajnart

ajnart commented Jul 24, 2026

Copy link
Copy Markdown
Member

@bensynapse

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants