Skip to content

issue/3887/feat/max-attainable-coverage#4537

Open
lsabor wants to merge 6 commits intomainfrom
issue/3887/feat/max-attainable-coverage
Open

issue/3887/feat/max-attainable-coverage#4537
lsabor wants to merge 6 commits intomainfrom
issue/3887/feat/max-attainable-coverage

Conversation

@lsabor
Copy link
Copy Markdown
Contributor

@lsabor lsabor commented Mar 25, 2026

closes #3887
adds max attainable coverage helper text to score and performance sections on question page

image image

Summary by CodeRabbit

  • New Features

    • Show maximum attainable peer-coverage next to user coverage with an info icon and tooltip linking to an explanation.
    • Inline max-coverage display added to participation summaries.
  • Localization

    • Added explanatory text for the coverage tooltip in English, Czech, Spanish, Portuguese, Traditional Chinese, and Chinese.
  • Bug Fixes

    • Removed stray debug console logging.

closes #3887
adds max attainable coverage helper text to score and performance sections on question page
todo: quivalent text on group info is cramped and hover doesn't work quite correclty
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Warning

Rate limit exceeded

@lsabor has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d8f511e-cb72-4747-a20c-224d58278a46

📥 Commits

Reviewing files that changed from the base of the PR and between a6b3163 and 3bb2c17.

📒 Files selected for processing (1)
  • front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
📝 Walkthrough

Walkthrough

Adds a computed max-attainable peer-coverage value and displays it inline as “(max. X%)” with an info-tooltip and help link; introduces i18n keys for the explanation and updates participation/additional-scores UI to render the suffix when applicable.

Changes

Cohort / File(s) Summary
Internationalization (non-English)
front_end/messages/cs.json, front_end/messages/es.json, front_end/messages/pt.json, front_end/messages/zh-TW.json, front_end/messages/zh.json
Added maxAttainableCoverageExplanation translation key; updated coverage summary strings to include a <maxCoverageDisplay></maxCoverageDisplay> insertion after {userCoverage}%.
English messages
front_end/messages/en.json
Added maxAttainableCoverageExplanation; updated participationSummaryCoverageBetterStats and participationSummaryCoverageWorseStats to append a <maxCoverageDisplay></maxCoverageDisplay> placeholder after {userCoverage}%.
Participation & Additional Scores UI
front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx, front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
Added local getMaxCoverage(question) helper to compute a 0–1 max-attainable coverage from question timestamps; render (max. X%) as maxCoverageDisplay when present, wrapped with a tooltip + info icon and help link; adjusted ScoreTable to accept an optional valueSuffix node.
Minor cleanup
front_end/src/components/question/score_card.tsx
Removed a stray console.log statement; no functional changes.

Sequence Diagram(s)

sequenceDiagram
    participant Q as Question data
    participant UI as ParticipationSummary / ScoresTable
    participant Tooltip as Tooltip (info icon + link)
    Q->>UI: provide open_time, scheduled_close_time, actual_close_time
    UI->>UI: compute getMaxCoverage(question)
    alt maxCoverage present
        UI->>UI: format "(max. X%)"
        UI->>Tooltip: render info icon with explanation + help link
        Tooltip-->>UI: show on hover/tap
    else no maxCoverage
        UI-->>UI: render nothing
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers

  • hlbmtc
  • elisescu
  • ncarazon

Poem

🐰
I hopped through strings and timestamp trails,
To mark the max your coverage entails.
An icon that whispers why ceilings exist,
A tooltip, a link — a forecaster's assist.
Hop, click, learn — a rabbit's tiny cheer!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning One unrelated change was identified: removal of a console.log statement in score_card.tsx is outside the scope of issue #3887's requirements for displaying max attainable coverage. Remove the console.log cleanup from score_card.tsx or address it in a separate PR to keep this change focused on max attainable coverage functionality.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'issue/3887/feat/max-attainable-coverage' references the linked issue and describes the feature, though it follows a branch-naming convention rather than a polished PR title.
Linked Issues check ✅ Passed The PR implements all primary coding requirements from issue #3887: updated i18n strings with max coverage display placeholders, added getMaxCoverage() helper function, and integrated tooltip UI with info icons in both participation_summary.tsx and additional_scores_table.tsx.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue/3887/feat/max-attainable-coverage

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx (1)

139-139: Use one shared max-coverage formatting rule across score surfaces.

This table uses one decimal place, while participation_summary.tsx rounds to an integer. Same metric, two displays.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
at line 139, The max-coverage percentage is formatted differently here
((maxCoverage * 100).toFixed(1) + "%") than in participation_summary.tsx;
introduce or use a single shared formatter (e.g., formatMaxCoverage or
formatPercentage) and replace the local (maxCoverage * 100).toFixed(1) usage in
additional_scores_table.tsx (and update participation_summary.tsx to the same
helper) so both score surfaces use the same rounding/display rule; ensure the
helper accepts the raw maxCoverage number and returns the final string with
percent sign for consistent rendering across components.
front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx (1)

42-55: Extract getMaxCoverage to a shared util.

The same helper now exists in two score components; centralizing it will prevent behavior drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/participation_summary.tsx
around lines 42 - 55, Extract the getMaxCoverage function (signature: const
getMaxCoverage = (question: QuestionWithForecasts): number | null) into a shared
utility module and replace the duplicate implementations with an import from
that module; specifically, create a shared util exporting getMaxCoverage, remove
the local getMaxCoverage definitions in both score components, update their
imports to import { getMaxCoverage } from the new util, and ensure the util
accepts the same QuestionWithForecasts shape and preserves the exact return
semantics (null when inputs missing or totalDuration <= 0).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@front_end/messages/cs.json`:
- Line 2084: Update the Czech locale strings to include the
<maxCoverageDisplay></maxCoverageDisplay> injection where the participation
summary sentence is defined so the max attainable coverage value is rendered;
specifically, edit the JSON entries that correspond to the participation summary
main sentence (the entries referenced in the review around the same region as
"maxAttainableCoverageExplanation") and insert the
<maxCoverageDisplay></maxCoverageDisplay> tag in the appropriate place in those
strings to mirror other locales.

In `@front_end/messages/pt.json`:
- Line 2082: The Portuguese locale is missing the {maxCoverageDisplay} insertion
in its participation summary strings, so add the placeholder where the
English/other locales show the “(max. X%)” text; update the relevant keys in
pt.json (e.g., the participation summary string(s) that accompany
"maxAttainableCoverageExplanation") to include the {maxCoverageDisplay} token in
the same position as other locales and ensure "maxCoverageDisplay" key
exists/has the correct translation alongside "maxAttainableCoverageExplanation".

In `@front_end/messages/zh-TW.json`:
- Line 2081: The Traditional Chinese locale is missing the {maxCoverageDisplay}
placeholder in the participation/coverage summary sentence templates (you
already have maxAttainableCoverageExplanation present); find the
participation/coverage summary keys in zh-TW (e.g., any keys like
participationSummary, coverageSummary, participationSummaryWithCount) and insert
the {maxCoverageDisplay} token where the inline max-coverage should appear so
the translated string mirrors the source template; keep surrounding punctuation
and spacing consistent with the EN source and do not alter
maxAttainableCoverageExplanation.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx:
- Around line 137-142: Replace the hardcoded English fragment "(max. ...)" in
the AdditionalScoresTable TSX with a localized string using the translations
hook (useTranslations) so the entire label is translatable; locate the JSX
around maxCoverage and Tooltip in additional_scores_table.tsx and wrap the whole
phrase in t or t.rich (passing the formatted value {(maxCoverage *
100).toFixed(1)}% as an interpolation) instead of the literal " (max. " and
closing ")". Ensure you remove the hardcoded punctuation and pass any
link/Tooltip content to t.rich to preserve existing behavior.
- Around line 90-99: getMaxCoverage can return non-finite or out-of-range
values; update the function (getMaxCoverage in additional_scores_table.tsx) to
validate that new Date(open_time), new Date(actual_close_time) and new
Date(scheduled_close_time) produce finite timestamps (guard with
isFinite/isNaN), ensure totalDuration > 0, compute rawCoverage = (actualClose -
open) / totalDuration and then clamp the returned value to the [0, 1] range
(return null for invalid/non-finite inputs). Keep the function signature and
null return behavior when inputs are invalid.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/participation_summary.tsx:
- Around line 46-55: getMaxCoverage currently computes (actualClose -
open)/totalDuration without validating timestamps or bounding the ratio; update
the getMaxCoverage function to first validate parsed dates (use isNaN or
Number.isFinite on new Date(...).getTime()) and return null for invalid
timestamps or non-positive totalDuration, then compute the ratio and clamp it to
the [0, 1] range before returning; reference getMaxCoverage and the variables
open_time, actual_close_time, scheduled_close_time so you update that helper and
ensure callers that render percentage text also treat null or out-of-range
values safely.
- Around line 120-121: The hardcoded "(max. " and "%" in
participation_summary.tsx prevent localization; update the component to use
useTranslations() (e.g., const t = useTranslations('ParticipationSummary')) and
replace the literal bits around Math.round(maxCoverage * 100) with a single
localized string key that accepts the percentage as an interpolated variable
(for example t('max', { pct: Math.round(maxCoverage * 100) })), or use separate
keys if your i18n prefers prefix/suffix; ensure you remove the hardcoded " (max.
" and "%" and add the corresponding i18n key(s) in your locale files.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx:
- Line 139: The max-coverage percentage is formatted differently here
((maxCoverage * 100).toFixed(1) + "%") than in participation_summary.tsx;
introduce or use a single shared formatter (e.g., formatMaxCoverage or
formatPercentage) and replace the local (maxCoverage * 100).toFixed(1) usage in
additional_scores_table.tsx (and update participation_summary.tsx to the same
helper) so both score surfaces use the same rounding/display rule; ensure the
helper accepts the raw maxCoverage number and returns the final string with
percent sign for consistent rendering across components.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/participation_summary.tsx:
- Around line 42-55: Extract the getMaxCoverage function (signature: const
getMaxCoverage = (question: QuestionWithForecasts): number | null) into a shared
utility module and replace the duplicate implementations with an import from
that module; specifically, create a shared util exporting getMaxCoverage, remove
the local getMaxCoverage definitions in both score components, update their
imports to import { getMaxCoverage } from the new util, and ensure the util
accepts the same QuestionWithForecasts shape and preserves the exact return
semantics (null when inputs missing or totalDuration <= 0).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84f009e2-8b93-45ce-9895-72ad32824d1c

📥 Commits

Reviewing files that changed from the base of the PR and between f1c7c45 and 589efc8.

📒 Files selected for processing (9)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
  • front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx
  • front_end/src/components/question/score_card.tsx
💤 Files with no reviewable changes (1)
  • front_end/src/components/question/score_card.tsx

Comment on lines +137 to +142
<span className="text-sm text-gray-600 dark:text-gray-600-dark">
{" (max. "}
{(maxCoverage * 100).toFixed(1)}%
<Tooltip
tooltipContent={t.rich("maxAttainableCoverageExplanation", {
link: (chunks) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded English in max coverage label.

"(max. ...)" is hardcoded in TSX, so this fragment won’t localize correctly.

Based on learnings: Do not hardcode English strings in TSX components; prefer useTranslations() and i18n strings across components, avoiding hardcoded literals that stall localization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
around lines 137 - 142, Replace the hardcoded English fragment "(max. ...)" in
the AdditionalScoresTable TSX with a localized string using the translations
hook (useTranslations) so the entire label is translatable; locate the JSX
around maxCoverage and Tooltip in additional_scores_table.tsx and wrap the whole
phrase in t or t.rich (passing the formatted value {(maxCoverage *
100).toFixed(1)}% as an interpolation) instead of the literal " (max. " and
closing ")". Ensure you remove the hardcoded punctuation and pass any
link/Tooltip content to t.rich to preserve existing behavior.

Comment on lines +120 to +121
{" (max. "}
{Math.round(maxCoverage * 100)}%
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

(max. …) label is still hardcoded English.

This prevents full localization of the participation summary string.

Based on learnings: Do not hardcode English strings in TSX components; prefer useTranslations() and i18n strings across components, avoiding hardcoded literals that stall localization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/participation_summary.tsx
around lines 120 - 121, The hardcoded "(max. " and "%" in
participation_summary.tsx prevent localization; update the component to use
useTranslations() (e.g., const t = useTranslations('ParticipationSummary')) and
replace the literal bits around Math.round(maxCoverage * 100) with a single
localized string key that accepts the percentage as an interpolated variable
(for example t('max', { pct: Math.round(maxCoverage * 100) })), or use separate
keys if your i18n prefers prefix/suffix; ensure you remove the hardcoded " (max.
" and "%" and add the corresponding i18n key(s) in your locale files.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4537-issue-3887-feat-max-attainable-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:issue-3887-feat-max-attainable-coverage-3bb2c17
🗄️ PostgreSQL NeonDB branch preview/pr-4537-issue-3887-feat-max-attainable
Redis Fly Redis mtc-redis-pr-4537-issue-3887-feat-max-attainable

Details

  • Commit: 3bb2c175e2faa08584f817237f1b31fbece99a53
  • Branch: issue/3887/feat/max-attainable-coverage
  • Fly App: metaculus-pr-4537-issue-3887-feat-max-attainable

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

Copy link
Copy Markdown
Contributor

@ncarazon ncarazon left a comment

Choose a reason for hiding this comment

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

LGTM with minor suggestions.

Comment on lines +78 to +87
const getMaxCoverage = (question: QuestionWithForecasts): number | null => {
const { open_time, actual_close_time, scheduled_close_time } = question;
if (!open_time || !actual_close_time || !scheduled_close_time) return null;
const open = new Date(open_time).getTime();
const actualClose = new Date(actual_close_time).getTime();
const scheduledClose = new Date(scheduled_close_time).getTime();
const totalDuration = scheduledClose - open;
if (totalDuration <= 0) return null;
return (actualClose - open) / totalDuration;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

getMaxCoverage is defined identically in both additional_scores_table.tsx and participation_summary.tsx. There's already a utils.ts in the same post_score_data/ directory – consider extracting getMaxCoverage there and importing it in both files.

Comment on lines +138 to +139
<span className="cursor-help text-sm text-gray-600 dark:text-gray-600-dark">
(max. {(maxCoverage * 100).toFixed(1)}%
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The max coverage display uses (maxCoverage * 100).toFixed(1) here (one decimal), but participation_summary.tsx uses Math.round(maxCoverage * 100) (integer). The same value shows differently in two places on the same page – e.g., "max. 73.2%" in the scores table vs "max. 73%" in the participation summary. Should these be consistent?

className?: string;
variant?: Variant;
}> = ({ rows, className, variant = "auto" }) => (
}> = ({ rows, className }) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You removed "variant" usage but its still used by group_resolution_score_data/index.tsx:48. Confirm that does not cause any regression and remove variant prop completely instead of silently ignoring it.

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.

Show max attainable coverage in scores

3 participants