Skip to content

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

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

feat: Labor Hub /jobs/ — All Jobs wall + per-job detail pages#4751
aseckin wants to merge 17 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

    • Launches Labor Hub Jobs: jobs index wall, job detail pages with charts, job navigation, curated insights, metric overlays, wage/hours tiles, year toggles, tickers, and shareable preview cards (Save/X/LinkedIn). Adds OG/PNG share endpoint.
  • Localization

    • Adds complete Labor Hub Jobs translations for CS/EN/ES/PT/ZH-TW/ZH and reorders some existing keys.
  • UI Improvements

    • Improved chart coloring/gradients, responsive layouts, fonts, and carousel overlay sizing.
  • Accessibility

    • Enhanced chart a11y 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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 93fa37df-77f2-47e0-934e-5f31e9ffedf2

📥 Commits

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

📒 Files selected for processing (5)
  • front_end/src/app/(main)/labor-hub/jobs/[slug]/page.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/components/metric_comparison_axis.tsx
  • front_end/src/app/(main)/labor-hub/jobs/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • front_end/src/app/(main)/labor-hub/jobs/components/hub_cta_card.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/metric_comparison_axis.tsx
  • front_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsx

📝 Walkthrough

Walkthrough

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

Changes

Labor Hub Jobs Feature

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, thresholds, and helpers
front_end/src/app/(main)/labor-hub/jobs/helpers/wall_types.ts, exposure_thresholds.ts, metric_defs.ts, build_comment_url.ts, format.ts
WALL_YEARS/WallYear/WallJob types; exposure tercile thresholds (getExposureLevel, normalize) and RANGES; MetricKey/MetricDef and METRIC_DEFS with formatting; formatSignedPercent; buildCommentUrl helper.
Cached data fetching and sanitization
front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wall_data.ts, fetch_job_insights.ts, fetch_tile_tickers.ts, fetch_wage.ts
fetchWallData() produces WallJob forecast map; fetchJobInsights() assembles curated/comment/keyword insights with sanitization and truncation; fetchTileTickers() builds deduplicated per-job tickers; fetchWage() retrieves 2035 wage forecast.
OEWS history
front_end/src/app/(main)/labor-hub/jobs/data/oews_history.ts
OEWS_HISTORY dataset and getHistoricalPercentByYear(slug) derive historical percent-change series used for chart seeding.
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, 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
front_end/src/app/(main)/labor-hub/jobs/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 with active state opening MetricOverlay.
Metric comparison & overlay
front_end/src/app/(main)/labor-hub/jobs/components/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, header (rich), comparison axis, nature bullets, and bounds text from METRIC_DEFS.
Share card & SVG preview
front_end/src/app/(main)/labor-hub/jobs/components/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
front_end/src/app/(main)/labor-hub/components/question_cards/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 system updates
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 & question-card integration
front_end/src/app/(main)/labor-hub/sections/jobs_monitor.tsx, .../question_cards/question_card.tsx
ContextualBar optional href with link/non-link rendering; JobsMonitorSection headerActions CTA linking to jobs; QuestionCard headerActions prop and conditional header container.
Translations
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
  • 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

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

Details

  • Commit: 14e920b344b070b24fb47c3104a4b1eb76b95256
  • 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

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

🧹 Nitpick comments (1)
front_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsx (1)

46-50: ⚡ Quick win

Tile value formatting diverges from METRIC_DEFS used by the overlay/axis.

The same metric renders differently between this tile and MetricComparisonAxis/MetricOverlay (which use METRIC_DEFS[key].format):

  • aoe: tile shows ${value.toFixed(1)}%, but metric_defs.ts formats it as v.toFixed(1) (no %).
  • felten: tile shows value.toFixed(2) (unsigned), but metric_defs.ts prepends an explicit +/.

A user opening the overlay from a tile will see the same number formatted inconsistently (e.g., 16.4% vs 16.4). Consider reusing METRIC_DEFS[key].format here so the display stays canonical, or confirm the divergence is intentional.

🤖 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/components/exposure_metrics.tsx
around lines 46 - 50, The tile's formatValue function diverges from the
canonical METRIC_DEFS formatting used by MetricComparisonAxis/MetricOverlay
(e.g., "aoe" shows a percent in the tile but METRIC_DEFS[aoe].format does not,
and "felten" misses the explicit sign). Replace the custom formatting in
formatValue (function name: formatValue, file: exposure_metrics.tsx) by
delegating to METRIC_DEFS[key].format(value) so the tile uses the same canonical
format as MetricComparisonAxis/MetricOverlay; if METRIC_DEFS may be undefined
for a key, fall back to the current value.toFixed behavior to preserve safety.
🤖 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/zh-TW.json`:
- Line 2283: The localized string feedTileQuestionsRecentlyResolved uses 「解答」
but should use 「解決」 to match resolved-status terminology; update the value for
the key feedTileQuestionsRecentlyResolved so the message reads "最近解決了 # 個問題"
(replace 「解答」 with 「解決」) ensuring the plural ICU pattern and placeholders remain
unchanged.

In `@front_end/src/components/base_modal.tsx`:
- Line 68: The immersive modal variant (isImmersive) sets the wrapper to
overflow-hidden with sm:p-4 while the modal panel still uses h-svh on small
screens, causing clipping on 640–767px widths; update the wrapper/panel class
logic so that when isImmersive is true the small-screen behavior allows
scrolling or constrains the panel height: either make the wrapper use
overflow-y-auto at sm breakpoint when isImmersive, or change the panel’s h-svh
usage (the panel h-svh classes referenced around the modal panel) to a sm-safe
max-height (e.g., sm:max-h based on svh minus padding) so content isn’t clipped.
Ensure you adjust the className on the wrapper (where isImmersive is checked)
and the panel’s h-svh classes consistently.

---

Nitpick comments:
In `@front_end/src/app/`(main)/labor-hub/jobs/components/exposure_metrics.tsx:
- Around line 46-50: The tile's formatValue function diverges from the canonical
METRIC_DEFS formatting used by MetricComparisonAxis/MetricOverlay (e.g., "aoe"
shows a percent in the tile but METRIC_DEFS[aoe].format does not, and "felten"
misses the explicit sign). Replace the custom formatting in formatValue
(function name: formatValue, file: exposure_metrics.tsx) by delegating to
METRIC_DEFS[key].format(value) so the tile uses the same canonical format as
MetricComparisonAxis/MetricOverlay; if METRIC_DEFS may be undefined for a key,
fall back to the current value.toFixed behavior to preserve safety.
🪄 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: f12f52cc-098b-4dea-812a-7704e0b557b4

📥 Commits

Reviewing files that changed from the base of the PR and between d21c691 and 63bfbeb.

📒 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 as they are similar to previous changes (8)
  • front_end/src/app/(main)/labor-hub/jobs/components/share_card.tsx
  • 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/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/job_nav_strip.tsx
  • front_end/messages/es.json
  • front_end/messages/pt.json

Comment thread front_end/messages/zh-TW.json Outdated
Comment thread front_end/src/components/base_modal.tsx
… drop chart head

- Revert the default context labels in the comparison axis: non-current
  job labels are hidden again unless hovered (only the highlighted job and
  the hovered job show), fixing the overlapping clutter
- Soften edge-label alignment thresholds (8% / 92%) so a near-edge label
  like ~90% stays centered instead of being forced right
- Remove the "How <job> compares" line from the overlay header (and the
  now-unused currentName prop)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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.

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