Skip to content

feat: Labor Hub /jobs/ — All Jobs wall + per-job detail pages#4751

Open
aseckin wants to merge 19 commits into
mainfrom
labor-hub-extension
Open

feat: Labor Hub /jobs/ — All Jobs wall + per-job detail pages#4751
aseckin wants to merge 19 commits into
mainfrom
labor-hub-extension

Conversation

@aseckin

@aseckin aseckin commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two consumer-facing routes under the Labor Hub as SEO-friendly entry points to the same forecast data, derived from the Claude Design "Fast Food" prototype:

  • /labor-hub/jobs/ — Forecast Wall: a sized-by-impact tile grid of all 15 tracked occupations with a year toggle (2027 / 2030 / 2035) and a hover-only news-ticker preview on each tile
  • /labor-hub/jobs/[slug]/ — Job Detail page: breadcrumb hero with employment-forecast chart, Jump To carousel across all 15 jobs, Felten / MNA / AOE exposure metrics with HIGH/MED/LOW chips + tooltips, Curated Insights (data.ts override → top comments → keyword-matched comments fallback) with click-through to the source comment, Wages + economy-wide Hours bento, share card (1.91:1) with year toggle, Save Image PNG + Share on X

Data: 15 occupations get a slug field on data.ts; optional wage_post_id, curated_insights, keyword_aliases, and excluded_comment_ids available per job for future curation without code changes.

Reuse, no new packages: MultiQuestionLineChart for the hero chart, gradient-carousel for Jump To, MobileCarousel for the mobile bento, existing SCREENSHOT_SERVICE_API_URL for the Share Card OG image. JetBrains Mono added via next/font/google; salmon-900 added to the palette.

SEO: per-page generateMetadata + generateStaticParams (15 prebuilt detail pages), OpenGraph + Twitter card, JSON-LD (Dataset per job, ItemList on All Jobs).

i18n: ~30 new laborHubJobs* keys translated into all 6 locales; job names left in English.

The existing Labor Hub dashboard is untouched, and there is intentionally no link from /labor-hub/ to /labor-hub/jobs/ in this first pass — discoverability will be handled separately.

Test plan

  • cd front_end && bun run build succeeds, .next/server/app/(main)/labor-hub/jobs/page.js and [slug]/page.js are generated
  • /labor-hub/jobs/ renders the 15-tile wall; year toggle re-sorts sizes
  • Tile hover: ticker stops sliding, expands to a 3-line excerpt with bottom fade
  • Tickerless tiles render with no divider artefact
  • /labor-hub/jobs/software-developers/ renders with Felten chip = HIGH, tooltip on ? icon, Curated Insights populated, Wages + Hours bento, Share card with Save Image / Share on X
  • Comment-sourced insight click opens the source comment on the question page in a new tab
  • Jump To strip: left arrow hides at scrollLeft 0 (snap-to-edge), right arrow hides at end; arrows are bright in dark mode
  • Mobile (<md): bento becomes a 3-step swipeable carousel
  • Dark mode looks correct across both pages
  • view-source: on either route shows <title>, meta description, OG tags, Twitter card, and <script type="application/ld+json">

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Launches Labor Hub — jobs index wall, job detail pages with forecasts/charts, job navigation strip, curated insights, metric overlays, wage/hours tiles, tickers, and share preview + OG/PNG share endpoint.
  • Localization

    • Adds complete Jobs copy in CS/EN/ES/PT/ZH‑TW/ZH and adjusts key ordering.
  • UI Improvements

    • Enhanced chart coloring/gradients, responsive bento layout, share-card preview, carousel overlay sizing, and new mono font support.
  • Accessibility

    • Improved chart accessibility labels and baseline descriptions.

aseckin and others added 2 commits May 19, 2026 14:38
Adds two new consumer-facing routes under the Labor Hub:
- /labor-hub/jobs/ — Forecast Wall of 15 occupations with year toggle and
  hover-only news ticker
- /labor-hub/jobs/[slug]/ — statically generated job detail page with
  breadcrumb, year-stat tiles, forecast chart, Jump To carousel, Felten/MNA/AOE
  exposure metrics, Curated Insights (tiered: data.ts override → top comments
  on the job's post → keyword-matched sibling comments), wages + economy-wide
  hours bento, share card, and Hub CTA

Share card PNG and OG meta image are generated through the existing
screenshot-service pattern at /og/labor-hub/jobs/[slug]/. No new packages,
no changes to the existing Labor Hub dashboard. Per-page JSON-LD (Dataset
for job pages, ItemList for All Jobs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 of the consumer-friendly job pages, closing prototype-parity
gaps and the curated-insights UX feedback:

- JetBrains Mono wired through next/font and Tailwind; salmon-900
  added to the palette so tile text matches the Claude Design output
  pixel-for-pixel
- Forecast Wall tiles: prototype-exact font sizes / padding / hover
  overlay; ticker now slides by default with horizontal mask gradients
  and expands to a 3-line fade-bottom excerpt on hover; tile padding
  reserves space so the title is never overlapped by the ticker
- Hub CTA card simplified to match the surrounding page sections
  (white bg, no colored border)
- Jump To strip: arrows snap-to-edge on click (gradient-carousel
  clamps to 0 / maxScroll when within half a step) and use bright
  light-bg buttons in dark mode; first snap point is now exactly
  scrollLeft=0 after dropping the list's px-2 padding so the left
  arrow hides correctly
- Exposure metrics get a question-circle icon next to each HIGH/MED/
  LOW chip with a hover tooltip
- Wages / Hours mini-cards centered at md+, bigger numeric values
- Bento becomes a 3-step MobileCarousel (Embla) on mobile, grid on
  desktop
- Curated Insights: tiered fallback honours per-job
  excluded_comment_ids; markdown is stripped down to plain prose
  (tables, images, HTML entities, escapes); empty-after-strip
  comments are filtered out; comment-sourced items wrap in an anchor
  to /questions/<post_id>/#comment-<id> that opens in a new tab,
  with a visible underline + darker username on hover
- All anchors in the new pages explicitly opt out of the global
  globals.css underline rule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Labor Hub Jobs feature: typed job data and lookups, cached fetchers (wall, insights, tickers, wages), Jobs listing and detail pages with OG generation, UI components (wall, nav, metrics, curated insights, share/export), chart coloring/sizing, design-token and font updates, and i18n across locales.

Changes

Labor Hub Jobs

Layer / File(s) Summary
Data model and job definitions
front_end/src/app/(main)/labor-hub/data.ts
JobDefinition and curated-insight types; JOBS_DATA typed with required slug and optional metadata; exports ALL_JOB_SLUGS, getJobBySlug, getJobSlugByName, getJobShort.
Wall types and OEWS history
front_end/src/app/(main)/labor-hub/jobs/helpers/wall_types.ts, data/oews_history.ts
WALL_YEARS/WallYear/WallJob types; OEWS_HISTORY dataset and getHistoricalPercentByYear(slug) for chart seeding.
Exposure/metric foundations
.../helpers/exposure_thresholds.ts, metric_defs.ts, helpers/format.ts, helpers/build_comment_url.ts
Exposure tercile thresholds (getExposureLevel, normalize), RANGES, MetricKey/MetricDef and METRIC_DEFS with formatting, formatSignedPercent, and buildCommentUrl helper.
Cached fetchers and sanitizers
.../helpers/fetch_wall_data.ts, fetch_job_insights.ts, fetch_tile_tickers.ts, fetch_wage.ts
fetchWallData() builds WallJob forecasts; fetchJobInsights() assembles curated/comment/keyword insights with sanitization and truncation; fetchTileTickers() builds deduplicated per-job tickers; fetchWage() retrieves 2035 wage forecast.
Jobs listing page
front_end/src/app/(main)/labor-hub/jobs/page.tsx
AllJobsPage with localized metadata, ItemList JSON-LD, hero (t.rich accent), JobsWall, and HubCtaCard.
Job detail page
front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
Static params, localized metadata with OG URL, concurrent data fetching (wall/insights/wage), Dataset JSON-LD, YearStats, MultiQuestionLineChart seeded with historical values, JobNavStrip, BentoLayout, ShareCard, HubCtaCard.
OG share & screenshot route
front_end/src/app/og/labor-hub/jobs/[slug]/page.tsx, .../route/route.ts
ShareCardPreview rendering for OG page (revalidate=3600); GET route posts to screenshot service with 15s timeout, validates slug/year, returns PNG with cache and optional download filename.
Wall UI, sizing, navigation, tickers
front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx, job_nav_strip.tsx, hub_cta_card.tsx
JobsWall assigns deterministic tile sizes by forecast magnitude, year toggle, Link-wrapped tiles, deterministic masked ticker animations; JobNavStrip centers active pill on mount/update; HubCtaCard links to hub with CTA copy.
Detail support components
bento_layout.tsx, curated_insights.tsx, wage_tile.tsx, year_stats.tsx, exposure_metrics.tsx
BentoLayout responsive grid; CuratedInsights renders directional rows and optional external comment links; WageTile formats wage percent; YearStats shows year cards; ExposureMetrics renders metric tiles and opens MetricOverlay.
Metric comparison & overlay
metric_comparison_axis.tsx, metric_overlay.tsx
MetricComparisonAxis normalizes metric values and renders desktop axis dots + mobile bars; MetricOverlay shows immersive modal with metric source, rich header, comparison axis, nature bullets, and bounds text from METRIC_DEFS.
Share card & SVG preview
share_card.tsx, share_card_preview.tsx
ShareCardPreview builds 1200×630 SVG with forecast polyline, gradient and points; ShareCard constructs Twitter/LinkedIn intents and rasterizes SVG to PNG for download.
Chart enhancements
multi_line_chart.tsx, multi_question_line_chart.tsx, multi_line_chart.types.ts
Adds fillHeight, historicalLabelText/forecastLabelText props; per-series colorByValue → per-point pointColorFn and horizontal SVG linearGradient injection; DataPointCircle and ChangeBadge accept colorFn to derive per-point colors; skeleton & client wiring updates.
Design tokens and UI tweaks
front_end/src/constants/colors.ts, front_end/src/utils/fonts.ts, front_end/tailwind.config.ts, front_end/src/components/gradient-carousel.tsx, front_end/src/components/base_modal.tsx
Added METAC_COLORS.salmon[900]; JetBrains Mono font export and CSS var + Tailwind fontFamily entry; gradient-carousel gradientWidthClass prop and edge snapping; BaseModal immersive sizing/header-offset adjustments.
Jobs monitor & QuestionCard integration
sections/jobs_monitor.tsx, question_card.tsx
ContextualBar optional href with link/non-link rendering; JobsMonitorSection headerActions CTA linking to jobs; QuestionCard headerActions prop and conditional header container.
Localization
front_end/messages/{cs,en,es,pt,zh-TW,zh}.json
Adds full Labor Hub Jobs i18n blocks (navigation, page/hero, job detail, metric labels/tooltips, ring labels, curated insights, wages/hours text, share UI, metric explanations, chart a11y/baseline) and reorders adjacent keys where required.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ncarazon
  • cemreinanc
  • hlbmtc

"I’m a rabbit of code and cheer,
I hop through jobs both far and near,
Charts that glow and strings that sing,
SVG paws and a sharing spring,
Hooray — labor hub’s springtime is here!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch labor-hub-extension

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🧹 Nitpick comments (4)
front_end/messages/zh.json (1)

2242-2242: ⚡ Quick win

Consider replacing mathematical Unicode character with standard ASCII.

The mathematical bold capital X "𝕏" (U+1D54F) may not render correctly on all systems and fonts. For better i18n compatibility and accessibility, consider using:

  • Standard ASCII "X"
  • Regular emoji if branding is important
  • Or just remove the symbol since the text already says "在 X 分享" (Share on X)
♻️ Proposed fix for better compatibility
-  "laborHubJobsShareTweet": "𝕏 在 X 分享"
+  "laborHubJobsShareTweet": "X 在 X 分享"

Or simply:

-  "laborHubJobsShareTweet": "𝕏 在 X 分享"
+  "laborHubJobsShareTweet": "在 X 分享"
🤖 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 `@front_end/messages/zh.json` at line 2242, The string value for the message
key "laborHubJobsShareTweet" uses the mathematical bold capital X character (𝕏)
which can render inconsistently; update the value to use a standard ASCII "X"
(or an appropriate emoji or remove the symbol) so it reads e.g. "X 在 X 分享" or "在
X 分享" and ensure you modify the "laborHubJobsShareTweet" entry accordingly in
the JSON.
front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts (1)

39-41: 💤 Low value

Type assertion assumes post structure.

The type assertion as QuestionWithNumericForecasts[] | undefined assumes the group_of_questions.questions structure matches the expected type. If the runtime structure differs, this could cause errors in getValueForLabel.

Consider adding runtime validation or using a type guard to ensure type safety. However, if the post structure is well-known and validated elsewhere, the current approach is pragmatic.

🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts
around lines 39 - 41, The code currently asserts
post.group_of_questions?.questions as QuestionWithNumericForecasts[] | undefined
which can hide runtime shape mismatches; add a runtime type guard (e.g.,
isQuestionWithNumericForecastsArray) and use it to validate
post.group_of_questions?.questions before treating it as
QuestionWithNumericForecasts[] (or fall back to undefined/empty array), then
only call getValueForLabel with validated questions; reference the
variables/values questions, post.group_of_questions?.questions and the helper
getValueForLabel when locating where to add the check.
front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts (1)

42-70: 💤 Low value

Consider more robust HTML tag stripping.

The regex /<\/?[^>]+>/g on Line 59 won't match malformed tags like <script (without closing >). While React's default escaping mitigates XSS risk, using a proper HTML parser or a more defensive approach would be more robust for security-sensitive contexts.

For the current use case (extracting plain text for display in React components), the risk is low.

🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts around
lines 42 - 70, stripBody's HTML stripping uses the fragile regex /<\/?[^>]+>/g
which can miss malformed tags like "<script " — replace that step with a proper
HTML-to-text approach: in the browser use DOMParser or create a temporary
element (e.g., document.createElement and element.innerHTML = s; textContent) to
reliably remove tags and handle malformed markup, and provide a safe fallback
for non-browser environments (e.g., a more defensive regex that strips any "<"
to next ">" or trims trailing "<" fragments). Update the stripBody function to
call this parser-based sanitizer instead of the current regex so HTML tags
(including malformed ones) are robustly removed before decoding entities and
extracting the first paragraph.
front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts (1)

11-42: ⚡ Quick win

Duplicated sanitization logic across files.

The decodeEntities and strip functions are duplicated from fetch_job_insights.ts. Consider extracting these into a shared utility module to maintain consistency and reduce duplication.

The same security considerations apply here: double-decoding and incomplete HTML sanitization are low-risk given React's default escaping, but could be improved for defense in depth.

🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts around
lines 11 - 42, Extract the duplicated decodeEntities and strip functions into a
single shared utility module (e.g., sanitize or textUtils), export them, then
replace the local implementations in this file by importing those exported
functions; specifically remove the local decodeEntities and strip definitions
and import decodeEntities and strip where they are used to ensure one canonical
implementation (matching the existing implementation used in the other helper),
run tests / lint and verify behavior is unchanged.
🤖 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 `@front_end/messages/cs.json`:
- Line 2208: Update the value for the JSON key "laborHubJobsHeroLead" in
front_end/messages/cs.json by correcting the Czech typo: replace "Předpovědači"
with "Předpovídači" so the hero lead reads correctly; ensure you preserve the
rest of the string and surrounding punctuation/escaping.

In `@front_end/src/app/`(main)/labor-hub/jobs/components/exposure_metrics.tsx:
- Around line 117-131: Tooltip trigger is currently a non-focusable <span>,
making it inaccessible to keyboard users; replace it with a keyboard-focusable
<button type="button">. Locate the Tooltip usage (Tooltip component wrapping the
trigger) and replace the span element that has aria-label and className with a
<button type="button"> keeping the same aria-label, className, and child
FontAwesomeIcon; ensure no form submission by including type="button" and
preserve visual styling and tooltip props (tooltipContent, showDelayMs,
placement) so keyboard users can focus and activate the tooltip.

In `@front_end/src/app/`(main)/labor-hub/jobs/components/job_nav_strip.tsx:
- Around line 55-64: In job_nav_strip.tsx, the active Link (the job pill that
uses isActive and tone(item.value2035)) isn't exposed to assistive tech; add an
aria-current attribute to the Link element so screen readers announce the
current item (e.g., set aria-current="page" when isActive, otherwise omit or set
undefined) to the Link that points to `/labor-hub/jobs/${item.slug}/`.

In `@front_end/src/app/`(main)/labor-hub/jobs/components/jobs_wall.tsx:
- Around line 141-152: This control is a segmented toggle but currently uses
partial tab semantics; change the container from role="tablist" to role="group"
(or remove role) and on each button remove role="tab" and aria-selected and
instead add aria-pressed={y === year} so the buttons use toggle semantics; keep
the aria-label={t("laborHubJobsYearToggleLabel")} on the container and keep
onClick={() => setYear(y)} and key={y} as-is (references: WALL_YEARS, year,
setYear).

In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts:
- Around line 28-40: The decodeEntities function currently decodes numeric
entities then named entities which can double-decode sequences like "&amp;lt;"
into "<"; update decodeEntities so named entities (&amp;, &lt;, &gt;, &quot;,
&`#39`;, &apos;, &nbsp;) are replaced first and numeric/hex replacements (&`#x`...;
and &#...;) are applied afterwards, or alternatively add a clear docstring on
decodeEntities stating it must only be used for plain-text contexts and must not
be used with dangerouslySetInnerHTML; modify the implementation in
decodeEntities accordingly to prevent unintended double-decoding.

In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts:
- Around line 17-18: Normalize the incoming year parameter by validating it
against the canonical set of allowed wall years before any use in URL
construction or download filename generation: introduce a constant list (e.g.,
knownWallYears) and replace direct use of req.nextUrl.searchParams.get("year")
(the local variable year) with a normalized value that is coerced to a string
from the allowed list (falling back to "2035" or the closest valid year) and use
that normalizedYear for all subsequent URL and filename building (including the
download flag handling and wherever the year is interpolated later in this
module).
- Around line 37-44: The POST to screenshotEndpoint using fetch (the const r =
await fetch(...) call) has no timeout; wrap the request with an AbortController,
pass controller.signal into fetch, and start a setTimeout that calls
controller.abort() after a configured timeout (e.g., SCREENSHOT_SERVICE_TIMEOUT
or a sensible default). Clear the timeout on successful response, and handle the
abort case by catching the thrown error and returning/throwing a clear
timeout/error response from this route handler so upstream slowness cannot hang
the request.
- Around line 23-26: Guard the construction of screenshotEndpoint by validating
process.env.SCREENSHOT_SERVICE_API_URL before calling new URL; specifically,
check that SCREENSHOT_SERVICE_API_URL is defined and is a valid URL (e.g.,
attempt to construct a URL inside the existing try/catch or validate with a
small helper) and only then build screenshotEndpoint, otherwise throw or return
a structured JSON error so the existing error handling catches malformed/missing
env values; update the code that creates screenshotEndpoint (the new URL(...)
call) to live inside that validation block and reference
SCREENSHOT_SERVICE_API_URL and screenshotEndpoint accordingly.

---

Nitpick comments:
In `@front_end/messages/zh.json`:
- Line 2242: The string value for the message key "laborHubJobsShareTweet" uses
the mathematical bold capital X character (𝕏) which can render inconsistently;
update the value to use a standard ASCII "X" (or an appropriate emoji or remove
the symbol) so it reads e.g. "X 在 X 分享" or "在 X 分享" and ensure you modify the
"laborHubJobsShareTweet" entry accordingly in the JSON.

In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts:
- Around line 42-70: stripBody's HTML stripping uses the fragile regex
/<\/?[^>]+>/g which can miss malformed tags like "<script " — replace that step
with a proper HTML-to-text approach: in the browser use DOMParser or create a
temporary element (e.g., document.createElement and element.innerHTML = s;
textContent) to reliably remove tags and handle malformed markup, and provide a
safe fallback for non-browser environments (e.g., a more defensive regex that
strips any "<" to next ">" or trims trailing "<" fragments). Update the
stripBody function to call this parser-based sanitizer instead of the current
regex so HTML tags (including malformed ones) are robustly removed before
decoding entities and extracting the first paragraph.

In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts:
- Around line 11-42: Extract the duplicated decodeEntities and strip functions
into a single shared utility module (e.g., sanitize or textUtils), export them,
then replace the local implementations in this file by importing those exported
functions; specifically remove the local decodeEntities and strip definitions
and import decodeEntities and strip where they are used to ensure one canonical
implementation (matching the existing implementation used in the other helper),
run tests / lint and verify behavior is unchanged.

In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts:
- Around line 39-41: The code currently asserts
post.group_of_questions?.questions as QuestionWithNumericForecasts[] | undefined
which can hide runtime shape mismatches; add a runtime type guard (e.g.,
isQuestionWithNumericForecastsArray) and use it to validate
post.group_of_questions?.questions before treating it as
QuestionWithNumericForecasts[] (or fall back to undefined/empty array), then
only call getValueForLabel with validated questions; reference the
variables/values questions, post.group_of_questions?.questions and the helper
getValueForLabel when locating where to add the check.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8ca2956-cc8c-40d2-afd1-7fc7b588a830

📥 Commits

Reviewing files that changed from the base of the PR and between b916d47 and 8ce93b0.

📒 Files selected for processing (32)
  • 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)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/curated_insights.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/hub_cta_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card_preview.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/wage_hours_cards.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsx
  • front_end/src/app/(main)/labor-hub/jobs/helpers/build_comment_url.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/exposure_thresholds.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wall_data.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/wall_types.ts
  • front_end/src/app/(main)/labor-hub/jobs/page.tsx
  • front_end/src/app/og/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts
  • front_end/src/components/gradient-carousel.tsx
  • front_end/src/constants/colors.ts
  • front_end/src/utils/fonts.ts
  • front_end/tailwind.config.ts

Comment thread front_end/messages/cs.json Outdated
Comment thread front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx Outdated
Comment thread front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
Comment thread front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts Outdated
Comment thread front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts Outdated
Comment thread front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4751-labor-hub-extension-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:labor-hub-extension-685df39
🗄️ PostgreSQL NeonDB branch preview/pr-4751-labor-hub-extension
Redis Fly Redis mtc-redis-pr-4751-labor-hub-extension

Details

  • Commit: df7fa484f2dc18170f39146bdb7700a0283864b8
  • Branch: labor-hub-extension
  • Fly App: metaculus-pr-4751-labor-hub-extension

ℹ️ 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

…ity fixes

Round 3 of the consumer-friendly job pages.

Layout & visuals
- Job Detail hero and Jump-To strip share one container with a horizontal
  divider, matching the prototype
- All Jobs hero and tile wall now share one container with a divider too
- "survive AI?" in the hero h1 picks up the prototype's blue-600 accent via
  t.rich
- Mobile breadcrumb drops the 3rd crumb (job name) so it doesn't wrap
- Mobile wall is now a 3-col uniform grid, no size variation, no tickers
- Bento on mobile: 5-tile grid (Felten, MNA, AOE, Wage, Hours); Curated
  Insights moves to its own full-width section below
- Wage / Hours cards center contents on md+, left-align on mobile,
  consistent fonts across all 5 tiles
- Share card preview switches to container queries (cqw units) so it scales
  proportionally on narrow viewports instead of squeezing
- HIGH chip text in dark mode bumped to salmon-900-dark for readability
- Chart card height reduced ~32px

Jump-To strip
- Prototype-matched chip style: uniform blue-100 / blue-900 active, no
  per-job color
- Mobile: arrows + "Jump to:" label hidden, gradient fade tightened
- Scroll position persisted across visits via sessionStorage; active pill
  is auto-scrolled into view via requestAnimationFrame after restore
- aria-current on the active pill; data-active-pill attribute for the
  ensure-visible lookup
- Arrows use items-center + w-9 h-9 so the FontAwesome icon centers
  correctly; light/dark bg flips via blue-900 / blue-900-dark
- gradient-carousel: new optional gradientWidthClass prop (defaults to
  current w-[152px]); arrow snap-to-edge in scrollByAmount so clicking
  prev near scrollLeft 0 lands exactly at 0 (canPrev threshold bumped to
  > 1 for sub-pixel forgiveness)

Chart
- Reverted to MultiQuestionLineChart for typography + hover behavior
- New historicalLabelText / forecastLabelText props on the underlying chart
  let the section labels be overridden; Job Detail uses "BASELINE"
- Line + scatter color picked from 2035 direction (mc2/mc3/mc1) via
  getSeriesOptions
- MultiQuestionLineChartSkeleton accepts the same height prop (and a
  showTitlePlaceholder flag) so the loading state matches the rendered
  size exactly — no page jump on hydration

A11y & reliability (from the GHAS / CodeQL pass)
- Tooltip trigger is now a <button type="button"> with a visible focus
  ring instead of a non-focusable <span>
- Year toggle is role="group" + aria-pressed (was tablist/aria-selected
  with no associated panels)
- decodeEntities (insights + tickers) is now a single-pass replacement so
  "&amp;lt;" decodes once to "&lt;" instead of double-decoding to "<";
  docstring notes plain-text-only usage
- OG route /og/labor-hub/jobs/[slug]/route hardened: ?year= clamped to
  WALL_YEARS allowlist, SCREENSHOT_SERVICE_API_URL validated before use
  (structured 500 on missing/invalid), fetch wrapped in an AbortController
  with a 15-second timeout (504 on abort)
- Czech translation fix: "Předpovědač" → "Předpovídač" across the three
  cs.json strings that contained it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts (2)

71-74: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid returning raw upstream error bodies to clients.

Passing upstream text through directly can leak internal service details. Return a stable generic error payload instead.

Proposed fix
     if (!r.ok) {
-      const text = await r.text();
-      return NextResponse.json({ error: text }, { status: r.status });
+      return NextResponse.json(
+        { error: "screenshot service failed" },
+        { status: r.status >= 400 && r.status < 600 ? r.status : 502 }
+      );
     }
🤖 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 `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts around lines 71 -
74, The handler currently returns raw upstream response bodies (using r.text())
which can leak internal details; instead, read the upstream body for server-side
logging (e.g., console.error or your logger) and return a fixed, generic JSON
error payload to the client via NextResponse.json while preserving the HTTP
status (use r.status). Update the branch that checks r.ok to stop forwarding raw
text, log the detailed text internally, and return a stable message such as {
error: "Upstream service error" } with NextResponse.json and status r.status.

62-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when the screenshot API key is missing.

This currently sends api_key: "" upstream, which turns misconfiguration into avoidable external failures instead of a clear local 500.

Proposed fix
+  const apiKey = process.env.SCREENSHOT_SERVICE_API_KEY;
+  if (!apiKey) {
+    return NextResponse.json(
+      { error: "screenshot service API key not configured" },
+      { status: 500 }
+    );
+  }
+
   try {
     const r = await fetch(screenshotEndpoint, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
-        api_key: process.env.SCREENSHOT_SERVICE_API_KEY || "",
+        api_key: apiKey,
       },
🤖 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 `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts around lines 62 -
65, The code currently sends api_key: "" when
process.env.SCREENSHOT_SERVICE_API_KEY is missing; update the route handler (the
GET request handler in route.ts) to validate
process.env.SCREENSHOT_SERVICE_API_KEY before making the upstream call and fail
fast with a 500/explicit error if it's falsy. Specifically, check
SCREENSHOT_SERVICE_API_KEY at the start of the handler and return an error
response (or throw) instead of continuing; then build the headers object with
api_key set to the validated value so you never send an empty string upstream.
🤖 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.

Outside diff comments:
In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts:
- Around line 71-74: The handler currently returns raw upstream response bodies
(using r.text()) which can leak internal details; instead, read the upstream
body for server-side logging (e.g., console.error or your logger) and return a
fixed, generic JSON error payload to the client via NextResponse.json while
preserving the HTTP status (use r.status). Update the branch that checks r.ok to
stop forwarding raw text, log the detailed text internally, and return a stable
message such as { error: "Upstream service error" } with NextResponse.json and
status r.status.
- Around line 62-65: The code currently sends api_key: "" when
process.env.SCREENSHOT_SERVICE_API_KEY is missing; update the route handler (the
GET request handler in route.ts) to validate
process.env.SCREENSHOT_SERVICE_API_KEY before making the upstream call and fail
fast with a 500/explicit error if it's falsy. Specifically, check
SCREENSHOT_SERVICE_API_KEY at the start of the handler and return an error
response (or throw) instead of continuing; then build the headers object with
api_key set to the validated value so you never send an empty string upstream.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65939d16-41d4-4d9c-ac1f-d6e56d0f4653

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce93b0 and 55ce5ce.

📒 Files selected for processing (20)
  • 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)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card_preview.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/wage_hours_cards.tsx
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts
  • front_end/src/app/(main)/labor-hub/jobs/page.tsx
  • front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts
  • front_end/src/components/gradient-carousel.tsx
✅ Files skipped from review due to trivial changes (2)
  • front_end/messages/pt.json
  • front_end/messages/es.json

aseckin and others added 2 commits May 28, 2026 12:39
CodeQL flagged "incomplete multi-character sanitization" on the
`/<\/?[^>]+>/` tag-stripper in fetch_job_insights.ts and
fetch_tile_tickers.ts: a single pass can be defeated (e.g. `<scr<b>ipt>`
re-forms `<script>`), and decodeEntities ran afterward so `&lt;script&gt;`
decoded into `<script>` with nothing left to strip it.

Drop the incomplete tag regex; decode entities first, then remove every
angle bracket so no HTML tag syntax can survive or re-form. The output is
rendered as plain text (React-escaped), so removing `<`/`>` is safe and
constitutes complete sanitization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing, scroll retention, share-card polish

Below Jump-To
- Drop the Hours tile; keep Wage only when the job has wage data
  (wage_post_id) — it sits above the 3 exposure tiles in the right rail
- New WageTile + trimmed fetch_wage helper; remove wage_hours_cards /
  fetch_wage_and_hours
- BentoLayout is one reflowing grid: desktop = Insights (2/3) + data rail
  (1/3); mobile = data rail then Insights in one container. Exposure tiles
  are 3-across on mobile, stacked on desktop

Jump-To strip
- Chip % numbers are bigger, bold, colored green/red by sign; inactive
  chips use blue-200
- Horizontal scroll now retained across job switches via an in-memory
  module variable + double-rAF restore; only a fresh visit auto-centers
  the active pill

Forecast chart (hero + share card)
- New opt-in colorByValue on the shared MultiLineChart: baseline gray,
  positive green, negative red, with a horizontal gradient blending
  mixed-sign segments; per-point dot colors and per-point hover/data-label
  badge colors
- yAxisGutter widened so two-digit negative labels show their minus sign;
  chart card fills the hero row height and centers the chart
- Tickers: animation delay folded into the shorthand (fixes the
  shorthand/longhand React warning); dedup so the same comment doesn't
  slide across many tiles

Share card
- Self-contained SVG with the per-value-colored forecast line + dashed
  gridlines behind the number; green dot removed; thicker/bolder line
- Client-side PNG export (SVG→canvas), no screenshot-service dependency;
  year toggle removed (always 2035)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aseckin aseckin requested a review from cemreinanc May 28, 2026 12:58

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 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
`@front_end/src/app/`(main)/labor-hub/components/question_cards/multi_line_chart.tsx:
- Around line 747-761: The gradient id generation in MultiLineChart currently
builds ValueGradient.id using only item.id, which can collide across multiple
instances; update the component that renders the chart (MultiLineChart) to
create a stable per-instance prefix (e.g., from React's useId() or a generated
stableId) and prepend it to the gradient id (replace
`lh-value-grad-${item.id...}` with something like
`{instanceId}-lh-value-grad-${item.id...}`) and ensure the same composed id is
used wherever the gradient is referenced (e.g., in stroke: url(#...)) so each
chart instance has unique SVG ids.

In `@front_end/src/app/`(main)/labor-hub/jobs/components/bento_layout.tsx:
- Around line 8-19: In BentoLayout, the mobile ordering is wrong: mobile should
render dataRail before insights but currently renders {insights} first because
the Tailwind order classes only apply from md up; fix by changing the element
order or adding mobile order classes so that dataRail appears first on small
screens (e.g., ensure the container renders dataRail before insights or apply
classes like order-1/order-2 on the divs containing dataRail and insights
respectively while keeping md:order-* for desktop), targeting the JSX nodes that
render {insights} and {dataRail} in the BentoLayout function.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03598874-b8d1-404a-be51-50f73e82fb38

📥 Commits

Reviewing files that changed from the base of the PR and between 55ce5ce and cb5add1.

📒 Files selected for processing (22)
  • 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)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.types.ts
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card_preview.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/wage_tile.tsx
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wage.ts
  • front_end/src/app/og/labor-hub/jobs/[slug]/page.tsx
💤 Files with no reviewable changes (1)
  • front_end/src/app/(main)/labor-hub/data.ts
✅ Files skipped from review due to trivial changes (2)
  • front_end/messages/pt.json
  • front_end/messages/es.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • front_end/messages/en.json
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/messages/zh.json
  • front_end/messages/cs.json
  • front_end/messages/zh-TW.json
  • front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts

Comment thread front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsx Outdated
Comment thread front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
# Conflicts:
#	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
@aseckin aseckin marked this pull request as ready for review May 28, 2026 14:02
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- zh-TW feedTileQuestionsRecentlyResolved: 解答 -> 解決 (match resolved terminology)
- BaseModal: drop panel h-svh at the sm breakpoint (640-767px) where it
  clipped against the wrapper's sm:p-4; constrain to sm:max-h and scroll

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
front_end/src/app/(main)/labor-hub/jobs/helpers/metric_defs.ts (2)

21-67: ⚡ Quick win

Consolidate metric value formatting to one source of truth.

Line 25 / Line 40 / Line 55 define formatting here, while ExposureMetrics renders values via a separate formatValue(...) path. This split can drift and produce conflicting numeric displays across tiles vs overlay/axis. Prefer routing both surfaces through the same formatter contract.

🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/metric_defs.ts around lines
21 - 67, The per-metric inline format functions in METRIC_DEFS (the format
properties on keys "felten", "mna", "aoe") are diverging from the rendering path
used by ExposureMetrics/formatValue; unify them by moving formatting to a single
source-of-truth and wiring both consumers to it—either export a shared formatter
factory or replace the format properties with references to the central
formatValue implementation and update ExposureMetrics to call
METRIC_DEFS[metricKey].format (or vice versa) so tiles, overlays and axes all
use the exact same formatting logic.

69-70: ⚡ Quick win

Use the shared palette token instead of a hardcoded accent hex.

Line 70 hardcodes #f87248; this should come from the shared color token (salmon-900) to prevent style drift and keep theming centralized.

🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/metric_defs.ts around lines
69 - 70, Replace the hardcoded hex in the METRIC_ACCENT_WARM export with the
shared palette token `salmon-900`: update the `export const METRIC_ACCENT_WARM =
"`#f87248`";` line to reference the centralized color token (importing it if
necessary) so the accent uses the shared `salmon-900` value instead of a literal
hex.
🤖 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 `@front_end/src/app/`(main)/labor-hub/jobs/helpers/metric_defs.ts:
- Around line 21-67: The per-metric inline format functions in METRIC_DEFS (the
format properties on keys "felten", "mna", "aoe") are diverging from the
rendering path used by ExposureMetrics/formatValue; unify them by moving
formatting to a single source-of-truth and wiring both consumers to it—either
export a shared formatter factory or replace the format properties with
references to the central formatValue implementation and update ExposureMetrics
to call METRIC_DEFS[metricKey].format (or vice versa) so tiles, overlays and
axes all use the exact same formatting logic.
- Around line 69-70: Replace the hardcoded hex in the METRIC_ACCENT_WARM export
with the shared palette token `salmon-900`: update the `export const
METRIC_ACCENT_WARM = "`#f87248`";` line to reference the centralized color token
(importing it if necessary) so the accent uses the shared `salmon-900` value
instead of a literal hex.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 762c2d7d-7eca-46fe-b0bf-caf3dafdae65

📥 Commits

Reviewing files that changed from the base of the PR and between d21c691 and 5b8b0a2.

📒 Files selected for processing (23)
  • 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)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_comparison_axis.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_overlay.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsx
  • front_end/src/app/(main)/labor-hub/jobs/data/oews_history.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/format.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/metric_defs.ts
  • front_end/src/app/(main)/labor-hub/sections/jobs_monitor.tsx
  • front_end/src/components/base_modal.tsx
✅ Files skipped from review due to trivial changes (1)
  • front_end/src/app/(main)/labor-hub/jobs/helpers/format.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • front_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsx
  • front_end/src/app/(main)/labor-hub/sections/jobs_monitor.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/components/base_modal.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/messages/en.json
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/jobs/data/oews_history.ts
  • front_end/messages/es.json
  • front_end/messages/zh.json
  • front_end/messages/zh-TW.json
  • front_end/messages/cs.json
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/messages/pt.json

…ponsive fits

- Jobs Monitor: add a primary "Visit Jobs" button (right arrow) left of the
  "..." menu via a new optional QuestionCard headerActions slot; links to
  /labor-hub/jobs/. New i18n key laborHubJobsVisitCta x6.
- Jump-To strip: rewrite scroll handling — drop the retained-scroll/rAF logic
  and center the active chip instantly in an isomorphic layout effect, so it's
  always visible and pre-positioned with no post-load animation.
- Exposure overlay axis line: fix near-black-in-light (bg-blue-700 dark:
  bg-blue-950-dark); add a faint cursor-following blurred glow clipped to the line.
- Responsive fits: smaller wall tile numbers on narrow screens; smaller
  job-detail BY-year stat numbers; stack exposure tile label over chip on mobile.
- All Jobs page: float the tabs/subtitle/tiles in a transparent container below
  the hero box (no divider), wider on mobile.
- Jump-To: shrink mobile edge gradient 60px -> 15px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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 `@front_end/src/app/`(main)/labor-hub/sections/jobs_monitor.tsx:
- Around line 117-127: The Jobs Monitor currently includes Buttons/links with
href="/labor-hub/jobs/" (e.g., the headerActions Button in jobs_monitor.tsx and
the other CTAs that reference /labor-hub/jobs/*); remove the navigation by
deleting the href attributes (or converting the anchor-style Button into a
non-link Button) so they are non-navigable in this PR, and optionally add
disabled or remove onClick handlers to keep the visual CTA but prevent
navigation; ensure you update every occurrence that references
"/labor-hub/jobs/" (including the headerActions Button and the other CTAs around
the noted blocks) so no element in this component links to /labor-hub/jobs/ yet.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 233ebf8d-2013-427e-b3e1-28c8638a17c9

📥 Commits

Reviewing files that changed from the base of the PR and between d21c691 and 329f38b.

📒 Files selected for processing (25)
  • 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)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/question_card.tsx
  • front_end/src/app/(main)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_comparison_axis.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_overlay.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsx
  • front_end/src/app/(main)/labor-hub/jobs/data/oews_history.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/format.ts
  • front_end/src/app/(main)/labor-hub/jobs/helpers/metric_defs.ts
  • front_end/src/app/(main)/labor-hub/jobs/page.tsx
  • front_end/src/app/(main)/labor-hub/sections/jobs_monitor.tsx
  • front_end/src/components/base_modal.tsx
🚧 Files skipped from review as they are similar to previous changes (21)
  • front_end/src/app/(main)/labor-hub/jobs/helpers/format.ts
  • front_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsx
  • front_end/src/app/(main)/labor-hub/jobs/helpers/metric_defs.ts
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_overlay.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/page.tsx
  • front_end/src/components/base_modal.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/data/oews_history.ts
  • front_end/src/app/(main)/labor-hub/data.ts
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsx
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsx
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/cs.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx

Comment thread front_end/src/app/(main)/labor-hub/sections/jobs_monitor.tsx
- Vertically center the axis dots on the thicker background line
- Split the cursor glow into independent light/dark styles
- Scope the dot color change to the hovered dot only (was darkening every
  dot on axis hover); dark mode now lightens the hovered dot (blue-700-dark)
- Plus manual styling tweaks to the Labor Hub jobs pages

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aseckin and others added 2 commits June 9, 2026 11:17
# Conflicts:
#	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
- Rewrite the 2nd/3rd exposure-metric copy (hallucinated attributions/scales):
  2nd is now Manning & Aguirre "M&A vulnerability" (0–1, 3dp); 3rd is
  Massenkoff & McCrory "Anthropic Exposure" / AOE (0–100%). Felten gets the
  itemized title/source/bullet/box fixes.
- Remove the middle bullet from all three overlays (natureKeys -> [n1, n3]).
- Number formats: mna -> v.toFixed(3); aoe -> percentage (tile + overlay + axis).
- All copy updated across the 6 locale files; orphaned …Nature2 keys left in place.
- Year toggle: white background in light mode (matches dark mode).
- Invert the "Explore the full Labor Hub" CTA (dark in light / light in dark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 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 `@front_end/messages/es.json`:
- Line 2346: The string key laborHubJobsMnaNature2 in es.json currently
describes a "proporción… (0–100 %)" and uses AOE-like wording; update its
Spanish text to describe the MNA vulnerability score on a 0–1 scale (consistent
with laborHubJobsMnaAxisLabel "0–1" and laborHubJobsMnaBounds "0 a 1") and
rephrase the semantics to match vulnerability (e.g., higher values = greater
vulnerability due to exposure and low adaptive capacity) instead of a percentage
or AOE framing so the wording aligns with laborHubJobsMnaNature3 and the defined
bounds.

In `@front_end/messages/pt.json`:
- Line 2343: The string for laborHubJobsMnaNature2 is incorrect: it currently
describes task-share/AI usage (AOE) but should describe the Manning & Aguirre
(MNA) vulnerability score. Update the value for the key "laborHubJobsMnaNature2"
to a concise Portuguese description that MNA is a 0–1 vulnerability measure
combining AI exposure with worker adaptability (e.g., "MNA é a pontuação de
vulnerabilidade (0–1) que combina a exposição à IA com a adaptabilidade dos
trabalhadores"), ensuring it matches the axis label and bounds for MNA.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 788e5249-db1b-4094-8b60-afbb0593ec14

📥 Commits

Reviewing files that changed from the base of the PR and between 7145e2e and 685df39.

📒 Files selected for processing (10)
  • 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)/labor-hub/jobs/components/exposure_metrics.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/hub_cta_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/src/app/(main)/labor-hub/jobs/helpers/metric_defs.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • front_end/src/app/(main)/labor-hub/jobs/components/hub_cta_card.tsx
  • front_end/messages/zh-TW.json
  • front_end/messages/en.json
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx
  • front_end/messages/zh.json
  • front_end/messages/cs.json

"laborHubJobsMnaAxisLabel": "Vulnerabilidad de Manning & Aguirre (0–1)",
"laborHubJobsMnaNature1": "Medida académica que combina la exposición a la IA con la capacidad de adaptación de los trabajadores.",
"laborHubJobsMnaNature2": "El valor es la <strong>proporción de las tareas de esta ocupación (0–100 %)</strong> que aparecen en el uso medido de la IA.",
"laborHubJobsMnaNature3": "Las puntuaciones de vulnerabilidad se relacionan tanto con la probabilidad de desplazamiento (a través de la exposición a la IA) como con su carga (a través de la capacidad de adaptación).",

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if laborHubJobsMnaNature2 is referenced in the codebase
rg -n 'laborHubJobsMnaNature2' --type ts --type tsx --type js --type jsx

Repository: Metaculus/metaculus

Length of output: 91


Fix inconsistent MNA (Manning & Aguirre) Nature2 description
laborHubJobsMnaNature2 describes a “proporción… (0–100 %)”, but the MNA metric is defined with a 0–1 scale (laborHubJobsMnaAxisLabel “0–1”, laborHubJobsMnaBounds “0 a 1”). The text also reads like the AOE nature2 semantics rather than an MNA vulnerability score—update laborHubJobsMnaNature2 to match the 0–1 vulnerability meaning to avoid confusion.

🤖 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 `@front_end/messages/es.json` at line 2346, The string key
laborHubJobsMnaNature2 in es.json currently describes a "proporción… (0–100 %)"
and uses AOE-like wording; update its Spanish text to describe the MNA
vulnerability score on a 0–1 scale (consistent with laborHubJobsMnaAxisLabel
"0–1" and laborHubJobsMnaBounds "0 a 1") and rephrase the semantics to match
vulnerability (e.g., higher values = greater vulnerability due to exposure and
low adaptive capacity) instead of a percentage or AOE framing so the wording
aligns with laborHubJobsMnaNature3 and the defined bounds.

"laborHubJobsMnaSource": "Manning & Aguirre — Pontuação de vulnerabilidade",
"laborHubJobsMnaAxisLabel": "Vulnerabilidade de Manning & Aguirre (0–1)",
"laborHubJobsMnaNature1": "Medida acadêmica que combina a exposição à IA com a capacidade de adaptação dos trabalhadores.",
"laborHubJobsMnaNature2": "O valor é a <strong>parcela das tarefas desta ocupação (0–100%)</strong> que aparecem no uso medido de IA.",

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 | ⚡ Quick win

Incorrect metric description for Manning & Aguirre (MNA).

Line 2343 describes MNA as "the share of this occupation's tasks (0–100%) that appear in measured AI usage," but this is the definition of AOE (Anthropic Occupational Exposure), not MNA.

According to the PR objectives, MNA is the "Manning & Aguirre vulnerability" score (0–1 scale), which combines AI exposure with worker adaptability. The description at line 2343 appears to have been copied from the AOE metric description (line 2349).

The axis label (line 2341) and bounds (line 2345) correctly state MNA is 0–1, but the Nature2 description contradicts this by describing task percentages and AI usage—characteristics of the AOE metric, not MNA.

📝 Suggested correction

Replace line 2343 with a description that accurately reflects MNA's definition as a vulnerability measure combining exposure and adaptability, consistent with the other MNA strings:

-  "laborHubJobsMnaNature2": "O valor é a <strong>parcela das tarefas desta ocupação (0–100%)</strong> que aparecem no uso medido de IA.",
+  "laborHubJobsMnaNature2": "É uma <strong>medida de 0 a 1</strong> — valores mais altos indicam maior vulnerabilidade à automação, considerando tanto a exposição à IA quanto a capacidade de adaptação dos trabalhadores.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"laborHubJobsMnaNature2": "O valor é a <strong>parcela das tarefas desta ocupação (0–100%)</strong> que aparecem no uso medido de IA.",
"laborHubJobsMnaNature2": "É uma <strong>medida de 0 a 1</strong> — valores mais altos indicam maior vulnerabilidade à automação, considerando tanto a exposição à IA quanto a capacidade de adaptação dos trabalhadores.",
🤖 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 `@front_end/messages/pt.json` at line 2343, The string for
laborHubJobsMnaNature2 is incorrect: it currently describes task-share/AI usage
(AOE) but should describe the Manning & Aguirre (MNA) vulnerability score.
Update the value for the key "laborHubJobsMnaNature2" to a concise Portuguese
description that MNA is a 0–1 vulnerability measure combining AI exposure with
worker adaptability (e.g., "MNA é a pontuação de vulnerabilidade (0–1) que
combina a exposição à IA com a adaptabilidade dos trabalhadores"), ensuring it
matches the axis label and bounds for MNA.

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.

3 participants