Skip to content

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

Draft
aseckin wants to merge 3 commits into
mainfrom
labor-hub-extension
Draft

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

Conversation

@aseckin
Copy link
Copy Markdown
Contributor

@aseckin aseckin commented May 20, 2026

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
    • Launched Labor Hub jobs feature with detailed forecast pages for individual jobs
    • Added job metrics display (Felten exposure, AI usage, AOE score) with exposure levels
    • Introduced wages and hours projections for job categories
    • Added curated insights section with community commentary
    • Enabled sharing and image download functionality for job forecasts
    • Expanded localization support to 6 languages (Czech, Spanish, Portuguese, Traditional Chinese, Simplified Chinese)

Review Change Stack

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73422c31-a80e-4222-9673-020a2090e8e0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch labor-hub-extension

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.

// Drop blockquote and bullet prefixes at start of line
s = s.replace(/^\s*[>*\-+]\s*/gm, "");
// Strip HTML tags (e.g. <p>, <strong>)
s = s.replace(/<\/?[^>]+>/g, "");
s = s.replace(/^[\s\-=*]{3,}$/gm, "");
s = s.replace(/[*_`#]+/g, "");
s = s.replace(/^\s*[>*\-+]\s*/gm, "");
s = s.replace(/<\/?[^>]+>/g, "");
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: 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/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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

🚀 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-55ce5ce
🗄️ PostgreSQL NeonDB branch preview/pr-4751-labor-hub-extension
Redis Fly Redis mtc-redis-pr-4751-labor-hub-extension

Details

  • Commit: e59240bc2d10c42e63681329ff671c0b087a40db
  • 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>
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.

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

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