Uh oh!
There was an error while loading. Please reload this page.
feat(landing): port the v5 marketing landing onto staging - #535
feat(landing): port the v5 marketing landing onto staging#535Darkest-Teddy wants to merge 5 commits into
Conversation
Brings the v5 public site to `staging` WITHOUT the rest of
`feat/landing-v5-port`: that branch is 217 commits ahead of staging and
carries backend routes, services, agents and 15 migrations (including two
colliding 0032_* and two 0033_* basenames). None of that is needed to show
the new landing, so this ports the public surface only.
What's here:
- components/landing-v5/** + lib/landing/** (the landing engine, hero3d,
drag simulation and the Act sections)
- the (public) route group: the new / plus /team, /wiki, /gallery, /news
and /faq, which the v5 navbar links to and staging did not have
- components/marketing/** and components/companion/CompanionShell
- globals.css and app/layout.tsx (OG/metadata), lib/deployGuard.ts with
its matching test, lib/errorMessage.ts
- api.ts gains only `subscribeToNewsletter`; staging's backend already
mounts /api/newsletter
One deliberate divergence from upstream: marketing/SignInModal calls the
3-arg `setActiveUser`. Upstream passes a 4th `{ persist }` option that only
exists on that branch's UserContext, whose wider change also adds app-wide
session recovery and shell-route redirects. Changing auth for every screen
is out of scope for a landing port, so the option is dropped here and should
return with that UserContext. The call site carries the same note.
eslint-suppressions.json gains an entry for the new
components/marketing/SignInModal.tsx path; staging only suppressed the rule
at the old components/SignInModal.tsx path.
Verified: tsc --noEmit clean, and `next build` succeeds with staging's build
vars (DEPLOY_ENV/BACKEND_URL/NEXT_PUBLIC_API_URL/COOKIE_DOMAIN), emitting
/team, /wiki, /gallery, /news and /faq. eslint is clean except for a
pre-existing react-hooks/exhaustive-deps error in
settings/GradescopeConnect.tsx, which arrives untouched from origin/staging.
vitest was not run locally (needs Node 22; this machine is on 20) - CI runs it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>This pull request has been ignored for the connected project Preview Branches by Supabase. |
Warning Review limit reached
Next review available in:37 seconds Limit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (57)
📝 WalkthroughWalkthroughThe PR replaces the landing page with Landing v5, adds companion and careers pages, introduces shared public layouts and content, expands deployment metadata, and adds OAuth, newsletter, error-handling, CSS, and interaction infrastructure. ChangesLanding v5 experience
Companion and public pages
Platform and marketing infrastructure
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Visitor
participant LandingPage
participant useLanding
participant LandingEngine
participant CanvasAndWebGL
Visitor->>LandingPage: open landing page
LandingPage->>useLanding: initialize refs and state
useLanding->>LandingEngine: start landing lifecycle
LandingEngine->>CanvasAndWebGL: render hero and graph visuals
Visitor->>LandingPage: explore graph or submit newsletter
LandingPage->>useLanding: dispatch interaction
useLanding->>LandingEngine: update interaction state
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | 46f7930 | Commit Preview URL Branch Preview URL | Aug 19 2026, 09:19 PM |
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/lib/deployGuard.ts (1)
39-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winNormalize trailing slashes before environment classification.
A canonical API URL with a trailing slash classifies as unknown. Then
detectHostConfigMismatchreturnsnullfor a staging host wired tohttps://api.saplinglearn.com/. This bypasses the runtime guard for a valid URL spelling.Normalize URL fields before comparison. Add regression tests for trailing-slash API and site URLs.
Proposed fix
function classify( value: string | undefined, field: 'apiUrl' | 'cookieDomain' | 'siteUrl', ): FrontendEnv | null { - const v = (value ?? '').trim().toLowerCase();+ const v = (value ?? '')+ .trim()+ .toLowerCase()+ .replace(field === 'cookieDomain' ? /$/ : /\/+$/, ''); if (!v) return null; for (const env of Object.keys(FRONTEND_ENVS) as FrontendEnv[]) { if (FRONTEND_ENVS[env][field].toLowerCase() === v) return env;🤖 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 `@frontend/src/lib/deployGuard.ts` around lines 39 - 46, Update classify to normalize URL values by removing trailing slashes before comparing against FRONTEND_ENVS fields, while preserving existing trimming and case normalization. Ensure both API and site URL classifications recognize trailing-slash variants, and add regression coverage for trailing-slash API and site URLs.
🟡 Minor comments (24)
frontend/src/app/(public)/news/page.tsx-175-199 (1)
175-199: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe card link text does not match its destination.
Each card is a
<Link href="/#newsletter">and its call to action reads "Read article →". A reader who selects a headline arrives at the newsletter signup on the home page, not at the post. All six cards lead to the same anchor.Change the call to action to state the real destination, for example "Get notified →", until article routes exist.
🤖 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 `@frontend/src/app/`(public)/news/page.tsx around lines 175 - 199, The news cards’ CTA text incorrectly promises article navigation while each Link in the posts.map block targets /#newsletter. Update the CTA span text from “Read article →” to wording that reflects newsletter signup, such as “Get notified →”, while preserving the existing link destination and styling.frontend/src/app/(public)/about/page.tsx-80-84 (1)
80-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUpdate the copy that points to a feedback button in the navbar.
The paragraph tells the reader to use "a feedback button in the navbar". The ported chrome in
frontend/src/components/companion/CompanionShell.tsxrenders Home, About, Team, Wiki, Gallery, News, FAQ, GitHub, Ko-fi, Sign In, and Get Started. It has no feedback control. Point the reader at a control that exists, for example the GitHub link or a contact address.🤖 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 `@frontend/src/app/`(public)/about/page.tsx around lines 80 - 84, Update the paragraph near the prose content to remove the nonexistent navbar feedback-button reference, and direct readers to an available control such as the GitHub link or a contact address instead. Preserve the surrounding message about reporting issues and sharing ideas.frontend/src/app/(public)/team/page.tsx-18-22 (1)
18-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTwo companion routes omit
alternates.canonical. The port sets a canonical URL onfrontend/src/app/(public)/about/page.tsx,frontend/src/app/(public)/careers/page.tsx, andfrontend/src/app/(public)/careers/[slug]/page.tsx, but not on these two routes. Without a canonical URL, these pages can be indexed under query-string or alternate-host variants.
frontend/src/app/(public)/team/page.tsx#L18-L22: addalternates: { canonical: '/team' }to themetadataexport.frontend/src/app/(public)/wiki/page.tsx#L18-L22: addalternates: { canonical: '/wiki' }to themetadataexport.🤖 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 `@frontend/src/app/`(public)/team/page.tsx around lines 18 - 22, Update the metadata exports in frontend/src/app/(public)/team/page.tsx lines 18-22 and frontend/src/app/(public)/wiki/page.tsx lines 18-22 to include alternates.canonical, using /team for the team route and /wiki for the wiki route.frontend/src/app/(public)/careers/[slug]/page.tsx-18-22 (1)
18-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe meta description appends an ellipsis even when it truncates nothing.
Line 20 always appends
…afterjob.description.slice(0, 155). If a description is shorter than 155 characters, the full sentence still ends with a truncation marker. Search results then show a complete sentence that looks cut off.Append the ellipsis only when the slice removed text.
🐛 Proposed fix
+ const blurb =+ job.description.length > 155 ? `${job.description.slice(0, 155)}…` : job.description; return { title: `${job.title} — Careers`, - description: `${job.department} · ${job.location} · ${job.type}. ${job.description.slice(0, 155)}…`,+ description: `${job.department} · ${job.location} · ${job.type}. ${blurb}`, alternates: { canonical: `/careers/${job.slug}` }, };🤖 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 `@frontend/src/app/`(public)/careers/[slug]/page.tsx around lines 18 - 22, Update the metadata construction in the careers page’s generateMetadata flow so the description appends “…” only when job.description exceeds 155 characters; leave shorter or exactly 155-character descriptions without a truncation marker while preserving the existing 155-character slice.frontend/src/app/(public)/wiki/page.tsx-28-31 (1)
28-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
scrollMarginTopis applied to the wrong element.The
H2style setsscrollMarginTop: 84. The TOC links on line 67 target the fragments#graph,#mastery,#review,#tutor,#ingestion,#grades, and#privacy. Those ids are on the<section>elements, not on the<h2>elements. The browser applies scroll margin to the element it scrolls into view, so the section top lands under the sticky 92px header fromCompanionShelland the heading is hidden.Move the offset to the sections.
🐛 Proposed fix
const H2: React.CSSProperties = { margin: 0, fontFamily: DISPLAY, fontWeight: 500, fontSize: 26, - lineHeight: 1.2, letterSpacing: '-0.015em', scrollMarginTop: 84,+ lineHeight: 1.2, letterSpacing: '-0.015em', }; +/** Clears the sticky companion header after an in-page anchor jump. */+const SECTION: React.CSSProperties = { scrollMarginTop: 104 };Then apply it to each section, for example:
- <section id="graph">+ <section id="graph" style={SECTION}>🤖 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 `@frontend/src/app/`(public)/wiki/page.tsx around lines 28 - 31, Move scrollMarginTop: 84 from the H2 style to the section styling used by the sections targeted by the TOC fragment links (`#graph`, `#mastery`, `#review`, `#tutor`, `#ingestion`, `#grades`, and `#privacy`). Keep the heading typography unchanged and ensure each target section receives the offset so it clears the sticky header.frontend/src/components/companion/CompanionShell.tsx-86-108 (1)
86-108: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winApply the responsive nav rules to
CompanionShell. The media query is scoped to.landing-dc, but theCompanionShellroot does not have that class. The rules do not match, despite using!important, so the compact nav remains hidden and the tab row remains visible below 1180px.🤖 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 `@frontend/src/components/companion/CompanionShell.tsx` around lines 86 - 108, Update the CompanionShell root element to include the landing-dc class so the existing responsive media-query rules apply to its nav-compact and nav-tabs elements. Preserve the current navigation structure and styling.Source: Linters/SAST tools
frontend/src/lib/landing/engine/dom.ts-49-73 (1)
49-73: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winA layout measurement taken before layout is ready is cached permanently. Both sites store a measured size on first read and re-measure only when a resize signal arrives. If the first read happens before the element has its final box, the wrong value is kept for the rest of the session. Reject an implausible measurement instead of caching it.
frontend/src/lib/landing/engine/dom.ts#L49-L73: do not assignrec.__cvwhenworhis 0.invalidateCanvas()runs only on a viewport size change, so a zero-size cache leaves the canvas blank.frontend/src/lib/landing/engine/marquee.ts#L136-L144: do not acceptm.setWwhen the summed childoffsetWidthis 0. The re-measure trigger is awrap.clientWidthchange, so a gap-only width leaves the track wrapping over a few pixels.🤖 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 `@frontend/src/lib/landing/engine/dom.ts` around lines 49 - 73, Reject zero-sized measurements before caching them: in frontend/src/lib/landing/engine/dom.ts lines 49-73, ensure cv does not assign rec.__cv when w or h is 0; in frontend/src/lib/landing/engine/marquee.ts lines 136-144, ensure the marquee measurement does not call m.setW when the summed child offsetWidth is 0. Preserve normal caching and measurement behavior for positive dimensions.frontend/src/lib/landing/engine/sim.ts-330-369 (1)
330-369: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuard the SVG structure assumptions during build.
Three unchecked assumptions read the authored SVG:
- Line 332:
ring.previousElementSibling as SVGCircleElement. If the ring is the first child,glowisnull. Line 839 then callsp.glow.setAttributeon every frame and throws aTypeError.- Line 333 and 345:
labelis dereferenced immediately bylabel.getAttribute('y'). A missing next sibling throws duringensureInit().- Line 362-367:
near()returns-1when the cluster haslineelements but no[data-sim]rings.nodes[b]is thenundefinedand line 367 throws.
holdArms()at line 653 andstepGroup()at line 752 already guard withif (!s0 || !t0) continue;, so missing nodes are anticipated later in the pipeline but not at build time. Skip a ring or a line when its expected siblings are absent.🤖 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 `@frontend/src/lib/landing/engine/sim.ts` around lines 330 - 369, Update the SVG build logic in the rings and lines loops to validate required structure before creating simulation entries: skip rings when the previous glow element or next label element is missing, and skip lines when either endpoint lookup in near() returns -1. Keep valid ring and line processing unchanged, ensuring no nodes or links are created with null siblings or invalid endpoint indices.frontend/src/lib/landing/engine/marquee.ts-69-114 (1)
69-114: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winBind
pointerupandpointercancelonwindowas well.
setPointerCaptureis called inside atry/catchat line 80. If the capture fails, later pointer events are not retargeted totrack. A drag that ends outsidetrackthen never runsend().m.dragstays non-null, soupdate()skips both drift and momentum for that track and the cursor staysgrabbinguntil the nextpointerdown.
frontend/src/lib/landing/engine/sim.tslines 507-510 already binds the release handlers on both the element andwindowfor this reason. Apply the same pattern here.🛡️ Proposed fix
track.addEventListener('pointerdown', onDown); track.addEventListener('pointermove', onMove); track.addEventListener('pointerup', onUp); track.addEventListener('pointercancel', onCancel); + window.addEventListener('pointerup', onUp);+ window.addEventListener('pointercancel', onCancel); cleanups.push(() => { wrap?.removeEventListener('pointerenter', onEnter); wrap?.removeEventListener('pointerleave', onLeave); track.removeEventListener('pointerdown', onDown); track.removeEventListener('pointermove', onMove); track.removeEventListener('pointerup', onUp); track.removeEventListener('pointercancel', onCancel); + window.removeEventListener('pointerup', onUp);+ window.removeEventListener('pointercancel', onCancel); STATE.delete(track); });🤖 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 `@frontend/src/lib/landing/engine/marquee.ts` around lines 69 - 114, Bind the existing onUp and onCancel release handlers to window in addition to track, matching the dual registration pattern used by sim.ts. Keep the current track listeners and end() behavior unchanged so drags are finalized even when pointer capture fails or the pointer leaves the track.frontend/src/components/landing-v5/BetaModal.tsx-244-260 (1)
244-260: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse
type="email"for the email field.The field is labelled "Email address" and validated as an email, but it is declared
type="text". That suppresses the email keyboard on mobile and blocks browser autofill. The manual check at lines 86-96 continues to work unchanged.🔧 Proposed fix
ref={inputRef} - type="text"+ type="email"+ autoComplete="email"+ inputMode="email" aria-label="Email address"🤖 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 `@frontend/src/components/landing-v5/BetaModal.tsx` around lines 244 - 260, Update the email input in BetaModal’s form to use type="email" instead of type="text", while preserving its existing validation, value handling, and other attributes.frontend/src/components/landing/useLanding.ts-593-597 (1)
593-597: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReport invalid input instead of returning silently.
If
valuehas no@, the function returns without settingsubscribeErrorand without any other state change. The user presses the button and sees no response. An empty non-requiredtype="email"field passes browser validation and reaches this branch.🐛 Proposed fix
const value = email.trim(); - if (!value.includes('@') || subscribing) return;+ if (subscribing) return;+ if (!value.includes('@')) {+ setSubscribeError('Enter a valid email address.');+ return;+ }🤖 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 `@frontend/src/components/landing/useLanding.ts` around lines 593 - 597, Update subscribeNow so invalid email input, including an empty trimmed value or a value without "@", sets subscribeError to the existing user-facing validation message before returning. Keep the subscribing guard and valid-input flow unchanged, ensuring the user receives feedback instead of a silent no-op.frontend/src/components/landing-v5/Navbar.tsx-74-79 (1)
74-79: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe navbar stays interactive while it is invisible.
While
heroMountedis false, opacity is 0 butpointerEventsremains'auto'. For the first ~1.9s of the intro, every navbar control is focusable and clickable while nothing is visible.♿ Proposed fix
opacity: exploring ? 0 : heroMounted ? 1 : 0, - pointerEvents: exploring ? 'none' : 'auto',+ pointerEvents: exploring || !heroMounted ? 'none' : 'auto',+ visibility: heroMounted ? 'visible' : 'hidden',🤖 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 `@frontend/src/components/landing-v5/Navbar.tsx` around lines 74 - 79, Update the navbar style logic in the visible component so pointerEvents is set to 'none' whenever the navbar is invisible, including while heroMounted is false and while exploring is true; restore 'auto' only when the navbar is visible and interactive.frontend/src/app/(public)/page.tsx-91-100 (1)
91-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winClear the subscription error when the modal closes.
state.subscribeErroris shared byBetaModalandJournaland is never reset. After a failed attempt, the message stays visible in the Journal form and reappears the next time the modal opens.useLandingalready exposesactions.resetSubscribeErrorfor this, and it is currently unused.🐛 Proposed fix
- onClose={() => setBetaOpen(false)}+ onClose={() => { actions.resetSubscribeError(); setBetaOpen(false); }}🤖 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 `@frontend/src/app/`(public)/page.tsx around lines 91 - 100, Update the BetaModal onClose handler to call actions.resetSubscribeError before closing the modal with setBetaOpen(false), ensuring the shared subscription error is cleared when the modal closes.frontend/src/app/globals.css-1563-1569 (1)
1563-1569: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStylelint fails on the quoted
Spectralfamily name.
font-family-name-quotesreports four errors: Lines 1565, 1713, 1784, and 1836.Spectralis a single identifier, so it needs no quotes.🔧 Proposed fix (apply to all four sites)
- font-family: var(--font-spectral), 'Spectral', Georgia, serif;+ font-family: var(--font-spectral), Spectral, Georgia, serif;As per static analysis hints,
font-family-name-quotesis enabled for this file.Also applies to: 1713-1713, 1784-1784, 1836-1836
🤖 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 `@frontend/src/app/globals.css` around lines 1563 - 1569, Remove the unnecessary quotes around the single-identifier Spectral font family name in all four font-family declarations, including the declarations associated with the visible landing-page style and the other reported sites, so they satisfy font-family-name-quotes.Source: Linters/SAST tools
frontend/src/components/landing-v5/lab/RoomsDemo.tsx-42-54 (1)
42-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe typing indicator and the delayed reply can name different partners.
sendcapturespartnerin the timeout closure. The indicator at Line 127 reads the livepartner. If the user changes the select during the 1600 ms delay, the indicator shows the new partner and the message arrives from the old one.♻️ Proposed fix
const [partner, setPartner] = useState<Partner>('maya'); + const partnerRef = useRef<Partner>('maya');+ partnerRef.current = partner;typeT.current = setTimeout(() => { - const reply = `my ${partner.charAt(0).toUpperCase() + partner.slice(1)} graph has eigenvalues green — want me to quiz you on it?`;+ const who = partnerRef.current;+ const reply = `my ${who.charAt(0).toUpperCase() + who.slice(1)} graph has eigenvalues green — want me to quiz you on it?`; setTyping(false); - setMsgs((m) => [...m, { who: partner, text: reply, mine: false }]);+ setMsgs((m) => [...m, { who, text: reply, mine: false }]); }, 1600);Also applies to: 125-132
🤖 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 `@frontend/src/components/landing-v5/lab/RoomsDemo.tsx` around lines 42 - 54, Update send so the delayed reply and typing indicator use the same partner snapshot captured when the message is sent. Store that snapshot in the relevant state or timeout flow, and ensure the indicator rendering near the partner display uses it until the delayed reply completes; preserve the existing behavior when the partner does not change.frontend/src/app/globals.css-1950-1957 (1)
1950-1957: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStylelint reports 45
keyframes-name-patternerrors in the ported blocks.The ported keyframe names are camelCase, for example
s1Stem,popHold,heroRise, andlabIn. The rule expects kebab-case. Renaming is not safe here, because the names are referenced from inlineanimationstrings in the landing components and inlabData-driven markup.Scope a rule disable to the ported region instead, so the rest of the file keeps the check.
🔧 Proposed approach
+/* stylelint-disable keyframes-name-pattern -- names are ported verbatim from the+ design components and are referenced by inline `animation` strings in TSX. */ `@keyframes` s1Stem { 0%,15% { stroke-dashoffset:27; } 34%,100% { stroke-dashoffset:0; } }Close the block with
/* stylelint-enable keyframes-name-pattern */after the last ported keyframe.As per static analysis hints,
keyframes-name-patternis enabled for this file.Also applies to: 2094-2096
🤖 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 `@frontend/src/app/globals.css` around lines 1950 - 1957, Scope-disable Stylelint’s keyframes-name-pattern rule immediately before the ported keyframe region containing names such as s1Stem, popHold, heroRise, and labIn, then re-enable it after the final ported keyframe. Keep the rule active for all other sections of globals.css and do not rename the animation identifiers.Source: Linters/SAST tools
frontend/src/lib/landing/course.ts-363-376 (1)
363-376: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle the singular case for
degree.If
degreeis 1, the text reads "its 1 neighbours". A sub-topic linked only to its parent produces that value.✏️ Proposed fix
- ' pulls this node plus its ' +- degree +- ' neighbours into context, then retrieves the chunks of your ' ++ ' pulls this node plus its ' ++ degree ++ (degree === 1 ? ' neighbour' : ' neighbours') ++ ' into context, then retrieves the chunks of your ' +🤖 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 `@frontend/src/lib/landing/course.ts` around lines 363 - 376, Update nodeUses so the text generated for degree uses singular “neighbour” when degree is 1 and plural “neighbours” otherwise, preserving the existing wording and behavior for all other values.frontend/src/components/landing-v5/lab/CardsDemo.tsx-46-53 (1)
46-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGuard the window key listener against editable targets.
The listener runs for every keydown on the window. It calls
e.preventDefault()for Space and rates the card for 1, 2, and 3. While this demo is mounted, a Space or a digit typed in any other input on the page is captured.🛡️ Proposed fix
const onKey = (e: KeyboardEvent) => { + const el = e.target as HTMLElement | null;+ if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))) return; if (e.key === ' ') { e.preventDefault(); flip(); }🤖 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 `@frontend/src/components/landing-v5/lab/CardsDemo.tsx` around lines 46 - 53, Update the keydown handler in the useEffect around onKey to ignore events whose target is an editable control, such as input, textarea, select, or contenteditable elements, before handling Space or rate keys; preserve the existing flip and rate behavior for non-editable targets.frontend/src/components/landing-v5/lab/GradesDemo.tsx-62-65 (1)
62-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe row percentage is not clamped, but the weighted grade is.
Line 25 clamps each score to
[0, r.possible]. Line 64 does not. If the user types 500 into a row worth 50, the row shows 1000% while the overall grade stays at the clamped value. Use the same clamp for both readouts.🐛 Proposed fix
const raw = parseFloat(earned[i]); - const pct = isNaN(raw) ? null : Math.round((raw / r.possible) * 100);+ const pct = isNaN(raw)+ ? null+ : Math.round((Math.max(0, Math.min(raw, r.possible)) / r.possible) * 100);🤖 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 `@frontend/src/components/landing-v5/lab/GradesDemo.tsx` around lines 62 - 65, Update the row percentage calculation in the GB_ROWS.map callback to clamp the parsed earned score to the range [0, r.possible] before computing the percentage, matching the existing clamping used for the weighted grade and keeping both readouts consistent.frontend/src/components/landing-v5/lab/NotesDemo.tsx-57-66 (1)
57-66: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe "No concepts found" sentinel corrupts the footer count.
Line 63 pushes
{ name: 'No concepts found', tier: 'unexplored' }into the sameconceptsarray that holds real matches. The footer at Lines 141-143 then readsconcepts.lengthand prints1 concepts linked to your MA 242 graph.after an empty extraction. The text is wrong, and the plural is wrong for a single match too.Track the empty result separately instead of encoding it as a concept.
🐛 Proposed fix
- const [concepts, setConcepts] = useState<{ name: string; tier: keyof typeof LAB_TIER }[]>([]);+ const [concepts, setConcepts] = useState<{ name: string; tier: keyof typeof LAB_TIER }[]>([]);+ const [ran, setRan] = useState(false);const found = MATCHERS.filter((m) => m.hit.test(lower)).map((m) => ({ name: m.name, tier: m.tier })); - setConcepts(found.length ? found : [{ name: 'No concepts found', tier: 'unexplored' }]);+ setConcepts(found);+ setRan(true); setBusy(false);- {concepts.length- ? `${concepts.length} concepts linked to your MA 242 graph.`- : 'Concepts you extract are linked to your graph, not just tagged on the note.'}+ {concepts.length+ ? `${concepts.length} concept${concepts.length === 1 ? '' : 's'} linked to your MA 242 graph.`+ : ran+ ? 'No concepts found in this note.'+ : 'Concepts you extract are linked to your graph, not just tagged on the note.'}Also applies to: 140-144
🤖 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 `@frontend/src/components/landing-v5/lab/NotesDemo.tsx` around lines 57 - 66, Update extract so an empty MATCHERS result is tracked separately rather than added to concepts; keep concepts containing only real matches and render the empty-state message independently. Adjust the footer rendering near the concepts count to display the correct zero count and use singular/plural wording based on the actual number of linked concepts.frontend/src/components/landing-v5/Closing.tsx-154-182 (1)
154-182: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winComposite ARIA roles are declared without their required child roles. Both sites put a composite role on a container and leave the children as plain
<button>elements. ARIA requires owned children with the matching role, so assistive technology announces an empty widget and the selected item is not conveyed.
frontend/src/components/landing-v5/Closing.tsx#L154-L182: addrole="menuitem"to each section button inside therole="menu"container.frontend/src/components/landing-v5/lab/QuizDemo.tsx#L104-L124: addrole="radio"andaria-checked={isPicked}to each option button inside therole="radiogroup"container.🤖 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 `@frontend/src/components/landing-v5/Closing.tsx` around lines 154 - 182, Add role="menuitem" to each section button rendered by SECTIONS.map in Closing.tsx (frontend/src/components/landing-v5/Closing.tsx#L154-L182). In QuizDemo.tsx (frontend/src/components/landing-v5/lab/QuizDemo.tsx#L104-L124), add role="radio" and bind aria-checked to isPicked on each option button within the radiogroup.frontend/src/components/landing-v5/lab/CalendarDemo.tsx-86-112 (1)
86-112: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winBlank leading cells are focusable empty buttons.
Every cell renders as a
<button>, including theLEAD_BLANKSplaceholders. The blank cells only setcursor: 'default', which does not remove them from the tab order. A keyboard user tabs through up to six empty buttons before reaching 1 October. The selected day also carries noaria-pressed, so the current selection is not announced.♿ Proposed change
<button key={i} onClick={() => { if (!c.blank) setDay(c.day as number); }} type="button" + disabled={c.blank}+ aria-hidden={c.blank || undefined}+ aria-pressed={c.blank ? undefined : selected}🤖 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 `@frontend/src/components/landing-v5/lab/CalendarDemo.tsx` around lines 86 - 112, Update the day-cell buttons rendered by the shown.map callback so blank placeholder cells are removed from keyboard focus, while retaining interactive keyboard behavior for real days. Add aria-pressed to each non-blank day button and set it according to the selected value so assistive technologies announce the current selection.frontend/src/components/landing-v5/Faq.tsx-71-76 (1)
71-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win"Read the full FAQ" links back to itself.
The anchor targets
#faq, and the enclosing<section>already hasid="faq"(Line 40). Clicking the link keeps the user on the same section, so the call to action does nothing.This PR adds the
/faqpublic route, andClosing.tsxLine 81 already links to it. Point this link at/faq.🔗 Proposed change
- <a href="`#faq`" style={{ color: '`#0C5638`', textDecoration: 'underline' }}>Read the full FAQ</a>+ <Link href="/faq" style={{ color: '`#0C5638`', textDecoration: 'underline' }}>Read the full FAQ</Link>Add the import:
import { FAQS } from '`@/lib/landing/content`'; +import Link from 'next/link'; import { DragField } from './DragField';🤖 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 `@frontend/src/components/landing-v5/Faq.tsx` around lines 71 - 76, Update the “Read the full FAQ” anchor in the Faq component to navigate to the public /faq route instead of the local `#faq` section, matching the existing link behavior in Closing.tsx.frontend/src/components/landing-v5/lab/GuideDemo.tsx-21-24 (1)
21-24: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAlign
RECENT.pickwith the course exam lists. NeitherMidterm 1 · Sep 30norQuiz 2 · Oct 03exists in its course'sGUIDES.exams. Opening either recent item sets an invalid select value while the guide heading displays that value.🤖 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 `@frontend/src/components/landing-v5/lab/GuideDemo.tsx` around lines 21 - 24, Update the RECENT entries in GuideDemo so each pick exactly matches an existing exam value in the corresponding course’s GUIDES.exams list, preserving the displayed recent-item behavior and ensuring the select value and guide heading use valid options.
🤖 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 `@frontend/src/app/`(public)/page.tsx:
- Around line 130-131: Update the onLearn handler in the page component to open
the tutor panel at GAL index 7 instead of index 2, while leaving onQuiz
unchanged. Prefer the named GAL lookup or constant from content.ts rather than a
hard-coded index so card ordering changes cannot reintroduce this mismatch.
In `@frontend/src/components/landing-v5/ActTutor.tsx`:
- Around line 92-109: Update the tutor pills container in ActTutor to remove its
buttons from keyboard navigation while hidden by binding the engine’s docked
state to inert (or visibility). Preserve the existing opacity and pointerEvents
behavior, and ensure the pills become focusable again when docked is false.
In `@frontend/src/components/landing-v5/BetaModal.tsx`:
- Around line 132-143: Update the HeroCard responsive layout to use a single
column below the medium breakpoint and the existing two-column structure at
wider widths, using its CSS class rather than an unconditional
gridTemplateColumns style. Enable vertical scrolling for constrained viewports,
and apply the 560px minimum height only at the wider breakpoint so mobile
content, including the email field and submit button, remains accessible.
In `@frontend/src/components/landing-v5/Gallery.tsx`:
- Around line 62-85: Update the Card component so non-ghost cards are
keyboard-focusable and expose button-like semantics, with a key handler that
triggers the existing delegated Track click behavior for Enter and Space. Keep
ghost cards inert by omitting focusability, interactive semantics, and
activation handling, while preserving the current data-tk targeting.
In `@frontend/src/components/landing-v5/lab/CardsDemo.tsx`:
- Around line 37-44: Update the rate callback to store its 300 ms timeout in a
ref instead of returning a cleanup function, and clear that timeout in an
unmount cleanup effect. Preserve the existing delayed setIdx behavior while
preventing it from firing after the component unmounts.
In `@frontend/src/components/landing-v5/navTheme.ts`:
- Around line 68-69: Update DARK_SECTIONS to contain only the section that
currently renders on a dark ground, act-graph, and revise its comment to
describe the single dark section accurately. Keep act-ingest and act-tutor out
of the list until their backgrounds are dark.
In `@frontend/src/components/landing/useLanding.ts`:
- Around line 218-229: Update the hero canvas effect around the lazy
`import('`@/lib/landing/engine/hero`')` to track whether cleanup has run, prevent
`startHeroCanvas` from starting after unmount, and stop it immediately if
initialization races with cancellation. Preserve the existing cleanup behavior
for normal mounted execution.
- Around line 277-282: Update the onKey handler in useLanding to handle Escape
while explore mode is active: after closing the gallery or jump menu as
appropriate, call exitExplore when s.current.exploring is true and neither
overlay was open. Ensure exitExplore is available at handler execution time
despite its later declaration, using the existing callback ordering or a ref as
needed.
In `@frontend/src/components/marketing/SignInModal.tsx`:
- Around line 130-153: Update the OAuth URL construction in the sign-in flow,
including sameTabUrl and popupUrl, to use the frontend-relative /api/auth/google
path instead of prepending API_URL. Preserve the existing popup_id query
parameter and same-tab fallback behavior so OAuth requests remain same-origin
and retain the session cookie.
In `@frontend/src/lib/landing/content.ts`:
- Around line 191-218: In frontend/src/lib/landing/content.ts lines 191-218,
mark the unavailable journal-founding.png and journal-ai-homework.png entries
with empty src values and isGraph: true so they use the graph fallback until
assets are shipped. In frontend/src/components/landing-v5/Journal.tsx lines
106-118, preserve the meta.isGraph branch that renders GraphThumb instead of
passing unusable paths to next/image.
In `@frontend/src/lib/landing/engine/sim.ts`:
- Around line 897-905: Update destroy() cleanup ordering so every cluster
element is re-parented to its original field before the cleanup that removes the
shell/overlay runs. Preserve the existing cleanup registration in ensureInit(),
but ensure repeated createSim() calls can still find the original
[data-dragnode] elements after destroy().
---
Outside diff comments:
In `@frontend/src/lib/deployGuard.ts`:
- Around line 39-46: Update classify to normalize URL values by removing
trailing slashes before comparing against FRONTEND_ENVS fields, while preserving
existing trimming and case normalization. Ensure both API and site URL
classifications recognize trailing-slash variants, and add regression coverage
for trailing-slash API and site URLs.
---
Minor comments:
In `@frontend/src/app/`(public)/about/page.tsx:
- Around line 80-84: Update the paragraph near the prose content to remove the
nonexistent navbar feedback-button reference, and direct readers to an available
control such as the GitHub link or a contact address instead. Preserve the
surrounding message about reporting issues and sharing ideas.
In `@frontend/src/app/`(public)/careers/[slug]/page.tsx:
- Around line 18-22: Update the metadata construction in the careers page’s
generateMetadata flow so the description appends “…” only when job.description
exceeds 155 characters; leave shorter or exactly 155-character descriptions
without a truncation marker while preserving the existing 155-character slice.
In `@frontend/src/app/`(public)/news/page.tsx:
- Around line 175-199: The news cards’ CTA text incorrectly promises article
navigation while each Link in the posts.map block targets /#newsletter. Update
the CTA span text from “Read article →” to wording that reflects newsletter
signup, such as “Get notified →”, while preserving the existing link destination
and styling.
In `@frontend/src/app/`(public)/page.tsx:
- Around line 91-100: Update the BetaModal onClose handler to call
actions.resetSubscribeError before closing the modal with setBetaOpen(false),
ensuring the shared subscription error is cleared when the modal closes.
In `@frontend/src/app/`(public)/team/page.tsx:
- Around line 18-22: Update the metadata exports in
frontend/src/app/(public)/team/page.tsx lines 18-22 and
frontend/src/app/(public)/wiki/page.tsx lines 18-22 to include
alternates.canonical, using /team for the team route and /wiki for the wiki
route.
In `@frontend/src/app/`(public)/wiki/page.tsx:
- Around line 28-31: Move scrollMarginTop: 84 from the H2 style to the section
styling used by the sections targeted by the TOC fragment links (`#graph`,
`#mastery`, `#review`, `#tutor`, `#ingestion`, `#grades`, and `#privacy`). Keep the heading
typography unchanged and ensure each target section receives the offset so it
clears the sticky header.
In `@frontend/src/app/globals.css`:
- Around line 1563-1569: Remove the unnecessary quotes around the
single-identifier Spectral font family name in all four font-family
declarations, including the declarations associated with the visible
landing-page style and the other reported sites, so they satisfy
font-family-name-quotes.
- Around line 1950-1957: Scope-disable Stylelint’s keyframes-name-pattern rule
immediately before the ported keyframe region containing names such as s1Stem,
popHold, heroRise, and labIn, then re-enable it after the final ported keyframe.
Keep the rule active for all other sections of globals.css and do not rename the
animation identifiers.
In `@frontend/src/components/companion/CompanionShell.tsx`:
- Around line 86-108: Update the CompanionShell root element to include the
landing-dc class so the existing responsive media-query rules apply to its
nav-compact and nav-tabs elements. Preserve the current navigation structure and
styling.
In `@frontend/src/components/landing-v5/BetaModal.tsx`:
- Around line 244-260: Update the email input in BetaModal’s form to use
type="email" instead of type="text", while preserving its existing validation,
value handling, and other attributes.
In `@frontend/src/components/landing-v5/Closing.tsx`:
- Around line 154-182: Add role="menuitem" to each section button rendered by
SECTIONS.map in Closing.tsx
(frontend/src/components/landing-v5/Closing.tsx#L154-L182). In QuizDemo.tsx
(frontend/src/components/landing-v5/lab/QuizDemo.tsx#L104-L124), add
role="radio" and bind aria-checked to isPicked on each option button within the
radiogroup.
In `@frontend/src/components/landing-v5/Faq.tsx`:
- Around line 71-76: Update the “Read the full FAQ” anchor in the Faq component
to navigate to the public /faq route instead of the local `#faq` section, matching
the existing link behavior in Closing.tsx.
In `@frontend/src/components/landing-v5/lab/CalendarDemo.tsx`:
- Around line 86-112: Update the day-cell buttons rendered by the shown.map
callback so blank placeholder cells are removed from keyboard focus, while
retaining interactive keyboard behavior for real days. Add aria-pressed to each
non-blank day button and set it according to the selected value so assistive
technologies announce the current selection.
In `@frontend/src/components/landing-v5/lab/CardsDemo.tsx`:
- Around line 46-53: Update the keydown handler in the useEffect around onKey to
ignore events whose target is an editable control, such as input, textarea,
select, or contenteditable elements, before handling Space or rate keys;
preserve the existing flip and rate behavior for non-editable targets.
In `@frontend/src/components/landing-v5/lab/GradesDemo.tsx`:
- Around line 62-65: Update the row percentage calculation in the GB_ROWS.map
callback to clamp the parsed earned score to the range [0, r.possible] before
computing the percentage, matching the existing clamping used for the weighted
grade and keeping both readouts consistent.
In `@frontend/src/components/landing-v5/lab/GuideDemo.tsx`:
- Around line 21-24: Update the RECENT entries in GuideDemo so each pick exactly
matches an existing exam value in the corresponding course’s GUIDES.exams list,
preserving the displayed recent-item behavior and ensuring the select value and
guide heading use valid options.
In `@frontend/src/components/landing-v5/lab/NotesDemo.tsx`:
- Around line 57-66: Update extract so an empty MATCHERS result is tracked
separately rather than added to concepts; keep concepts containing only real
matches and render the empty-state message independently. Adjust the footer
rendering near the concepts count to display the correct zero count and use
singular/plural wording based on the actual number of linked concepts.
In `@frontend/src/components/landing-v5/lab/RoomsDemo.tsx`:
- Around line 42-54: Update send so the delayed reply and typing indicator use
the same partner snapshot captured when the message is sent. Store that snapshot
in the relevant state or timeout flow, and ensure the indicator rendering near
the partner display uses it until the delayed reply completes; preserve the
existing behavior when the partner does not change.
In `@frontend/src/components/landing-v5/Navbar.tsx`:
- Around line 74-79: Update the navbar style logic in the visible component so
pointerEvents is set to 'none' whenever the navbar is invisible, including while
heroMounted is false and while exploring is true; restore 'auto' only when the
navbar is visible and interactive.
In `@frontend/src/components/landing/useLanding.ts`:
- Around line 593-597: Update subscribeNow so invalid email input, including an
empty trimmed value or a value without "@", sets subscribeError to the existing
user-facing validation message before returning. Keep the subscribing guard and
valid-input flow unchanged, ensuring the user receives feedback instead of a
silent no-op.
In `@frontend/src/lib/landing/course.ts`:
- Around line 363-376: Update nodeUses so the text generated for degree uses
singular “neighbour” when degree is 1 and plural “neighbours” otherwise,
preserving the existing wording and behavior for all other values.
In `@frontend/src/lib/landing/engine/dom.ts`:
- Around line 49-73: Reject zero-sized measurements before caching them: in
frontend/src/lib/landing/engine/dom.ts lines 49-73, ensure cv does not assign
rec.__cv when w or h is 0; in frontend/src/lib/landing/engine/marquee.ts lines
136-144, ensure the marquee measurement does not call m.setW when the summed
child offsetWidth is 0. Preserve normal caching and measurement behavior for
positive dimensions.
In `@frontend/src/lib/landing/engine/marquee.ts`:
- Around line 69-114: Bind the existing onUp and onCancel release handlers to
window in addition to track, matching the dual registration pattern used by
sim.ts. Keep the current track listeners and end() behavior unchanged so drags
are finalized even when pointer capture fails or the pointer leaves the track.
In `@frontend/src/lib/landing/engine/sim.ts`:
- Around line 330-369: Update the SVG build logic in the rings and lines loops
to validate required structure before creating simulation entries: skip rings
when the previous glow element or next label element is missing, and skip lines
when either endpoint lookup in near() returns -1. Keep valid ring and line
processing unchanged, ensuring no nodes or links are created with null siblings
or invalid endpoint indices.
---
Nitpick comments:
In `@frontend/src/app/`(public)/careers/CareersList.tsx:
- Around line 217-284: Update the collapsible panel style in the job card
rendering to include visibility based on isOpen: visible when expanded and
hidden when collapsed. Add visibility to the existing transition so the state
changes with the panel animation while keeping the Apply for this role link and
other content inaccessible during collapse.
- Around line 51-55: Replace both raw img elements in CareersList with the
next/image component, matching the existing usage in CompanionShell for
/sapling-icon.svg. Preserve the current alt text and styling, and set the second
occurrence’s dimensions to width={20} and height={20} while retaining the first
occurrence’s 26×26 sizing.
In `@frontend/src/app/`(public)/faq/page.tsx:
- Around line 60-89: Update the FAQ answer panel associated with the button
using item.i: assign it a unique id, add matching aria-controls to the button,
and include visibility in the panel styles so it is hidden when collapsed and
visible when open. Preserve the existing max-height and opacity transitions.
In `@frontend/src/app/`(public)/news/page.tsx:
- Around line 111-153: Update the category menu logic around the existing
outside pointerdown listener to handle Escape while the menu is open, close it,
and return focus to the trigger button. Add a ref to the trigger button and
focus it both when Escape dismisses the menu and when selecting an option via
the NEWS_FILTERS buttons, while preserving the existing outside-click behavior.
In `@frontend/src/components/companion/CompanionShell.tsx`:
- Around line 84-141: Wrap the primary link group currently rendered by the
`nav-tabs` container in a `<nav>` landmark with a descriptive `aria-label`,
closing it after the GitHub and Ko-fi links. Also locate the header’s footer
link row and wrap it in a separate appropriately labelled `<nav>` element,
preserving the existing links and layout.
In `@frontend/src/components/landing-v5/ActIngest.tsx`:
- Around line 259-282: Add aria-hidden="true" to the decorative flying chip
elements rendered in the CHIPS.map callback within ActIngest, ensuring assistive
technologies ignore these duplicate labels while leaving their visual animation
and styling unchanged.
In `@frontend/src/components/landing-v5/ActTutor.tsx`:
- Around line 95-97: Add type="button" to the pill button rendered in ActTutor’s
button map alongside its existing key and onClick props, matching the explicit
button type used by the other buttons in this PR.
In `@frontend/src/components/landing-v5/Closing.tsx`:
- Line 92: Replace the hardcoded “© 2026” values in the Closing component with a
single dynamically derived current year, and reuse that value in both copyright
locations near the existing spans.
In `@frontend/src/components/landing-v5/DragField.tsx`:
- Around line 70-103: Update the label branch in the nodes mapping within
DragField’s render flow to always create a text element, including when label is
missing. Preserve the glow, ring, and label ordering and use an empty
placeholder for missing label content while retaining valid label positioning
and styling.
In `@frontend/src/components/landing-v5/Faq.tsx`:
- Around line 84-121: Update the FAQ trigger and answer panel so each button in
the mapped FAQ item has an aria-controls reference matching that panel’s unique
id. Add hidden or equivalent visibility handling to the panel when open is
false, while preserving the existing expanded state and visual transition
behavior.
In `@frontend/src/components/landing-v5/FeatureLab.tsx`:
- Around line 41-50: Add dialog semantics to the fixed overlay in FeatureLab by
assigning role="dialog", aria-modal="true", and an accessible name tied to the
panel heading or another existing label; implement focus movement to the close
button when open and restore focus when closed. In the tool rail, add an
explicit active-state attribute such as aria-current or aria-selected to the
open tool instead of relying on background color alone.
In `@frontend/src/components/landing-v5/Gallery.tsx`:
- Around line 87-109: Add an explicit accessible role to the div rendered by
Track so its existing aria-label is announced by assistive technology; use
role="group" without changing the current card rendering or interaction
behavior.
- Around line 71-73: Remove the redundant frame styling from the wrapper div
rendering GALLERY_MINIS[i] in Gallery: eliminate its duplicated height,
borderRadius, background, border, and overflow/position container behavior as
appropriate, while preserving the mini’s own frame and layout. Keep the
aria-hidden wrapper only if needed without adding a second visual frame.
In `@frontend/src/components/landing-v5/lab/CardsDemo.tsx`:
- Around line 100-115: Update the flip target around the onClick={flip} handler
to be keyboard accessible: prefer a button element, or add role="button",
tabIndex={0}, and an onKeyDown handler that invokes flip for Enter and Space
while preventing the default Space behavior. Preserve the existing click
interaction and visual styling.
In `@frontend/src/components/landing-v5/lab/GradesDemo.tsx`:
- Around line 74-83: Update the onChange sanitizer in the GradesDemo input to
allow at most one decimal point while preserving digits and the existing state
update behavior. Ensure values such as “1.2.3” are normalized so the displayed
text matches the value used by score computation.
In `@frontend/src/components/landing-v5/lab/GuideDemo.tsx`:
- Line 40: Use a consistent lookup style for the GUIDES map in the GuideDemo
component: update the initialization around set and the lookup in the relevant
course iteration around c so both rely on the CourseKey type without an
unnecessary nullish fallback, or both use the same fallback pattern. Preserve
the existing guide-selection behavior while removing the inconsistent
implication that GUIDES[course] may be missing.
In `@frontend/src/components/landing-v5/lab/NotesDemo.tsx`:
- Around line 57-80: Store the timers created by extract and summarize in refs
alongside saveT, clear their handles on unmount, and reset each ref after its
callback runs. Follow the existing timer-ref cleanup pattern in FeatureLabDemo
so callbacks cannot update state after the component unmounts.
In `@frontend/src/components/landing-v5/lab/QuizDemo.tsx`:
- Line 31: Extract the repeated q.options correctness check into a shared
predicate or derived value near the existing right calculation, then reuse it in
both locations. Update submit’s setStreak call to use the functional updater
form based on the previous streak value, preserving the existing
increment-or-reset behavior without reading streak from the render closure.
In `@frontend/src/components/landing-v5/lab/RoomsDemo.tsx`:
- Around line 76-106: Update the message-list container in RoomsDemo so it has
aria-live="polite" and automatically scrolls to the newest message after each
append. Use a ref and an effect tied to the message-list state, preserving the
existing rendering and styling.
In `@frontend/src/components/landing-v5/Navbar.tsx`:
- Around line 199-234: Remove the incomplete ARIA menu pattern from the navbar:
delete role="menu" from the panel and aria-haspopup="menu" from its trigger,
while preserving the existing links and open/close behavior. Expose the panel as
a plain navigation group without adding menuitem roles or keyboard navigation.
In `@frontend/src/components/landing/useLanding.ts`:
- Around line 383-399: Optimize both scanning effects in useLanding: replace the
first effect’s recurring setInterval(scan, 900) polling with a MutationObserver
watching root and invoking scan for relevant DOM additions, while preserving
cleanup and observer behavior. In the second effect around the section
descendant traversal, query only elements marked with the intended animation
attribute (such as [data-anim]) instead of '*', and retain the existing
animation logic for matched elements.
In `@frontend/src/lib/landing/companionContent.ts`:
- Around line 149-160: Update WIKI_TIERS.dot and WIKI_RATINGS.tone to store the
raw hex color values from TIER instead of CSS declaration strings. In the wiki
page consumer, remove the cssColor regex and parsing/fallback logic, and use
t.dot and r.tone directly as colors.
In `@frontend/src/lib/landing/content.ts`:
- Around line 38-135: Document that GAL’s array order is a positional contract
and must not be reordered or have entries inserted without updating callers. Add
and export a galIndexOf helper keyed by GalleryKind, then update the page.tsx
actions.openGal calls to resolve indices through that helper instead of
hard-coded positions.
In `@frontend/src/lib/landing/dragClusters.ts`:
- Line 36: Remove the redundant `as DragCluster[]` assertion from the
`DRAG_CLUSTERS` array literal, preserving its existing `DragCluster[]` type
annotation so excess-property and required-field checks remain enabled.
In `@frontend/src/lib/landing/engine/flip.ts`:
- Around line 24-37: Remove the unused ran field from the FlipState interface
and its initialization in createFlipState, and remove all assignments to st.ran,
including in armFlip and flipClose. Keep the actual animation guard in
useLanding.ts via flipRan unchanged.
In `@frontend/src/lib/landing/engine/plant.ts`:
- Around line 78-91: Update restore() to validate the parsed localStorage value
as an array, retain only string entries, and truncate each label to MAX_LABEL
before calling add() or updating labels. Ensure invalid or non-array data
produces an empty restored label set without partially processing it, while
preserving the seven seed nodes and normal return count.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
Uh oh!
There was an error while loading. Please reload this page.
| {/* the engine fades these in once the act's scroll is spent */} | ||
| <div data-tutor-pills="1" style={{ display: 'flex', gap: 8, marginTop: 8, opacity: 0, pointerEvents: 'none', transition: 'opacity 400ms ease' }}> | ||
| {CAPTIONS.map(([tag], i) => ( | ||
| <button | ||
| key={tag} | ||
| onClick={() => onSetMode(i)} | ||
| style={{ | ||
| border: '1px solid rgba(12,86,56,0.3)', borderRadius: 99, padding: '9px 18px', | ||
| ...MONO, fontSize: 10.5, letterSpacing: '0.12em', cursor: 'pointer', | ||
| background: tutorMode === i ? PILL_ON : 'transparent', | ||
| color: tutorMode === i ? PILL_ON_FG : '#33443B', | ||
| transition: 'all 250ms', | ||
| }} | ||
| > | ||
| {tag} | ||
| </button> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash# Find what the engine writes on the tutor pills element.
rg -n -C 6 'tutor-pills|tutorPills' --glob '*.ts' --glob '*.tsx'Repository: SaplingLearn/Sapling
Length of output: 158
🏁 Script executed:
#!/bin/bashset -e
printf'%s\n''--- candidate files ---'
fd -i 'ActTutor|tutor|landing-v5'.| head -80
printf'%s\n''--- all tutor-pills references ---'
rg -n -C 8 --hidden --glob '!node_modules' --glob '!dist' --glob '!build''tutor-pills|tutorPills|data-tutor'.printf'%s\n''--- ActTutor structure ---'
ACT=$(fd -t f -i 'ActTutor.tsx'.| head -1)if [ -n"$ACT" ];then
ast-grep outline "$ACT"
sed -n '1,180p'"$ACT"fiRepository: SaplingLearn/Sapling
Length of output: 19055
🏁 Script executed:
#!/bin/bashset -e
printf'%s\n''--- engine writes to M.pills ---'
rg -n -C 10 'M\.pills|pills\.(style|classList)|style\.(opacity|pointerEvents|visibility)|opacity.*pills|pills.*opacity' frontend/src/lib/landing frontend/src/components/landing-v5 --glob '*.ts' --glob '*.tsx'printf'%s\n''--- engine files and relevant structure ---'
fd -t f . frontend/src/lib/landing/engine -x sh -c 'echo --- "$1"; wc -l "$1"; ast-grep outline "$1" | head -80' sh
printf'%s\n''--- direct pills-related engine context ---'
rg -n -C 20 'pills|handover|spent|scroll' frontend/src/lib/landing/engine --glob '*.ts' --glob '*.tsx'Repository: SaplingLearn/Sapling
Length of output: 50377
Remove hidden pills from the keyboard tab order.
The engine changes only opacity and pointerEvents, so hidden buttons remain focusable. Toggle inert with the engine’s docked state, or toggle visibility there as well.
🤖 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 `@frontend/src/components/landing-v5/ActTutor.tsx` around lines 92 - 109,
Update the tutor pills container in ActTutor to remove its buttons from keyboard
navigation while hidden by binding the engine’s docked state to inert (or
visibility). Preserve the existing opacity and pointerEvents behavior, and
ensure the pills become focusable again when docked is false.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| /** | ||
| * Presentation metadata paired positionally with POSTS. | ||
| * | ||
| * NOTE: `journal-founding.png` and `journal-ai-homework.png` are not in | ||
| * `public/` yet — both exceed the design API's 256 KiB read cap, so they | ||
| * ship with the project export alongside the rest of the source. | ||
| */ | ||
| export const POST_META: PostMeta[] = [ | ||
| { | ||
| slot: 'journal-1', | ||
| src: '/journal-founding.png', | ||
| slotHint: 'Drop the founding photo', | ||
| isPhoto: true, | ||
| isGraph: false, | ||
| comments: 12, | ||
| likes: 84, | ||
| floatDelay: '0s', | ||
| }, | ||
| { | ||
| slot: 'journal-2', | ||
| src: '/journal-ai-homework.png', | ||
| slotHint: 'Drop an image for this essay', | ||
| isPhoto: true, | ||
| isGraph: false, | ||
| comments: 31, | ||
| likes: 126, | ||
| floatDelay: '-2.5s', | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Two journal images are declared but may not ship.POST_META points at /journal-founding.png and /journal-ai-homework.png, and the file's own comment states both assets are absent from public/. Journal.tsx renders those paths through next/image with no fallback, so two of the three journal cards can show a broken image.
frontend/src/lib/landing/content.ts#L191-L218: add the two PNG files topublic/, or setsrc: ''andisGraph: trueon those entries until the assets ship.frontend/src/components/landing-v5/Journal.tsx#L106-L118: keep themeta.isGraphbranch as the fallback path, so an entry without a usablesrcrendersGraphThumbinstead of a broken image.
📍 Affects 2 files
frontend/src/lib/landing/content.ts#L191-L218(this comment)frontend/src/components/landing-v5/Journal.tsx#L106-L118
🤖 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 `@frontend/src/lib/landing/content.ts` around lines 191 - 218, In
frontend/src/lib/landing/content.ts lines 191-218, mark the unavailable
journal-founding.png and journal-ai-homework.png entries with empty src values
and isGraph: true so they use the graph fallback until assets are shipped. In
frontend/src/components/landing-v5/Journal.tsx lines 106-118, preserve the
meta.isGraph branch that renders GraphThumb instead of passing unusable paths to
next/image.
Uh oh!
There was an error while loading. Please reload this page.
Jose-Gael-Cruz-Lopez
commented
Aug 19, 2026
Code review — v5 marketing landing onto stagingThis ports the v5 marketing landing onto FindingsP0[P0] Both CI jobs are red, and the failures hide the fact that nothing was typechecked or tested — Backend ( Frontend ( To be clear about attribution: neither is yours. Both reproduce verbatim at the base commit P1[P1] <SignInModalopen={signInOpen}onClose={()=>setSignInOpen(false)}/>
// middleware.ts:38if(errorCode)u.searchParams.set('error',errorCode)// middleware.ts:52 and :67if(!session)returnredirectToSignin(request,'session_expired')The page this replaces read that param and threaded it through — base [P1] The staging deploy will advertise production canonical and OG URLs — metadataBase: newURL(resolveSiteUrl(process.env)),…alternates: {canonical: '/'},
// deployGuard.ts:98-106exportfunctionresolveSiteUrl(env: EnvSource): string{constdeployEnv=(env.DEPLOY_ENV??'').trim().toLowerCase();if(deployEnv&&deployEnvinFRONTEND_ENVS)returnFRONTEND_ENVS[deployEnvasFrontendEnv].siteUrl;constexplicit=(env.NEXT_PUBLIC_SITE_URL??'').trim();if(explicit)returnexplicit.replace(/\/+$/,'');returnFRONTEND_ENVS.production.siteUrl;}
[P1] The landing runs a parallel colour palette that shares no value with the design tokens — Counting hex literals across Not one of these is a token. The core set in [P1] This is a stale snapshot of a landing already merged on
The dangerous part is that one of those divergences is invisible to the type checker: // this branch — hero3d/index.ts:52-56 (an intensity multiplier, 0..2)exportfunctionstartHeroRig(canvas,getMouse,getParallax: ()=>number)// origin/main — hero3d/index.ts:76-80 (a scroll offset in pixels)exportfunctionstartHeroRig(canvas,getMouse,getHeroShiftPx: ()=>number)Both are P2[P2] Three of the four new
[P2] After this PR the only [P2] A new eslint suppression was added for code introduced in this PR — "src/components/marketing/SignInModal.tsx": {
"react-hooks/set-state-in-effect": { "count": 1 }
},Style Guide §7 states the ratchet's purpose as "don't fix the whole backlog to land a change, but never add to it." The baseline exists to grandfather legacy violations; [P2] // The landing page keeps its own .landing-page class for the mesh-background visual.exportdefaultfunctionPublicLayout({ children }: {children: React.ReactNode}){return<divclassName="public-surface">{children}</div>;}No element in the tree carries .landing-page .sapling-mesh-blob { position: absolute; border-radius:50%; filter:blur(100px); … }
.landing-page .sapling-mesh-blob--1 { top:-10%; left:-10%; … }along with the other P3[P3] 1.3 MB of unoptimized PNGs committed —
What's good
Verdict: request changes — the auth-error regression and the staging-metadata bug are both user-visible, and the branch needs to be reconciled with the v5 landing already on Review-only pass — no code changed and nothing fixed. Conventions checked against the Canopy |
…PLOY_ENV
CI was red at the LINT step on both jobs, so tsc, pytest and vitest never
ran on this port at all.
- backend/models/gradescope.py: drop the unused pydantic Field import (F401).
- settings/GradescopeConnect.tsx: stabilise refreshAll with useCallback and
list it in the mount effect (exhaustive-deps is an error here). Keying on
userId also fixes the stale-account bug the empty dep array hid.
- (public)/page.tsx: read the ?error= code middleware.ts redirects with and
thread it into the modal, opening it when a code is present. Read through
useSyncExternalStore so the page stays statically prerenderable and no
setState lands in an effect; dismissing the modal strips the param.
- page.tsx: openGal indices now come from galIndexOf('quiz'/'tutor') — the
'Ask the tutor' button opened the notetaker panel. BetaModal onClose now
calls resetSubscribeError so a failed subscribe stops leaking into Journal.
- marketing/SignInModal.tsx: fix set-state-in-effect at source (reset during
render on the open prop change) and remove the suppression entry it was
given. OAuth now uses the frontend-relative /api/auth/google for both the
popup and the same-tab fallback, so the session cookie is not third-party.
- delete the orphaned components/SignInModal.tsx (zero importers) and point
the eslint data-testid rule at its replacement.
- deployGuard.classify: normalise trailing slashes on URL fields (not on
cookieDomain) so a valid slashed API/site URL cannot silently disarm the
guard. Five regression tests, all of which fail without the fix.
- middleware.ts: wire resolveFrontendEnv + detectHostConfigMismatch in, so a
host/backend mismatch reports env_misconfig instead of looping the user
through session_expired forever.
- wrangler.toml/package.json: set DEPLOY_ENV per environment so staging stops
advertising production canonical/OG URLs and the build-time guard is armed.
- globals.css/(public)/layout.tsx: delete the ~950 lines of .landing-page CSS
the replaced page made unreachable, fix the inverted comment, unquote the
single-identifier Spectral, and scope a keyframes-name-pattern disable to
the ported region only.The a11y fixes in this PR all defend behaviour that is invisible on screen and in review: a gallery card that only opens on click, a navbar that stays clickable while transparent, an accordion whose closed answers remain in the tab order, a role="radiogroup" containing no radios. Two lab-demo state bugs get the same treatment — a reply naming a different partner than the typing indicator, and a footer counting an empty-result sentinel as "1 concepts" — plus the galIndexOf contract that replaced the hard-coded GAL indices. Renamed from the scratch file that commit b6c3a8a swept in mid-edit.
- news: card CTA said 'Read article' while every card links to /#newsletter; it now says 'Get notified' and the Link carries a matching aria-label. The category listbox closes on Escape and returns focus to its trigger (also on option select) — dismissing it used to drop focus to <body>. - about: the copy sent readers to 'a feedback button in the navbar' that CompanionShell does not render; it now links the GitHub issue tracker. - team/wiki: add the alternates.canonical their siblings already had. - careers/[slug]: only append the meta-description ellipsis when slice(0,155) actually truncated, so short blurbs stop reading as cut off. - wiki: move scrollMarginTop from the H2 to the <section> elements — the TOC fragments target the sections, so anchors landed under the sticky header. Drop cssColor(): companionContent.ts now stores bare hex. - faq: unique panel ids + aria-controls, and 'visibility' in the collapsed style so collapsed answers are neither focusable nor announced. - careers/CareersList: same collapsed-panel fix, so 'Apply for this role' is not tabbable while closed; both raw <img> replaced with next/image. - CompanionShell: wrap the header link group and the footer link row in labelled <nav> landmarks. The compact-nav media query was scoped to .landing-dc, which this shell must never wear, so below 1180px the overflowing tab row stayed and the disclosure stayed hidden — unscoped it. - page.tsx: read ?signin=1 too. CompanionShell's header 'Sign In' links to /?signin=1 and nothing read it, so that control did nothing at all. - public/journal-*.png: 1.29 MB -> 432 KB (-67.8% / -64.6%) via libimagequant 256-colour quantisation with Floyd-Steinberg dither (same engine pngquant uses; RMSE 2.0 and 2.8 of 255). A fully lossless re-encode was tried first and was larger than the originals. Filenames and paths unchanged.
Jose-Gael-Cruz-Lopez
commented
Aug 19, 2026
Review fixes appliedEvery outstanding finding on this PR (human review + CodeRabbit) has been addressed and pushed. Blocker
Major
CodeRabbit majors
Minor / nitsDead Still open (deliberately not done)This branch is a stale snapshot of a landing already merged on Verification — Fixes applied and verified locally against this branch head; each figure above is a command I ran, not an estimate. |
Jose-Gael-Cruz-Lopez
commented
Aug 19, 2026
Verify against the real database before mergingI could not reach a live database while working on this — there are no credentials on this machine (only These are the checks that need a real connection. This branch now carries a backend change, via #565
I checked statically that every table the restored route touches exists on staging — SELECT table_name FROMinformation_schema.tablesWHERE table_schema ='public'AND table_name IN ('gradescope_credentials','gradescope_course_links','gradescope_links');All three should be present. Then exercise one endpoint end to end and confirm it resolves at a single Two things #565 deliberately left behindSELECTcount(*) FROM gradescope_links;
Ledger divergence between staging and mainStaging carries two migrations
Static verification only — no live database was reachable from this environment. Schema model built by replaying |
Puts the v5 marketing landing on
stagingwithout mergingfeat/landing-v5-port.Why not just merge the v5 branch
feat/landing-v5-portis 217 commits ahead oforigin/stagingand the diff is mostly not the landing page — 179 backend test files, 31 agents, 27 services, 17 routes, and 15 new migrations, including two colliding0032_*and two colliding0033_*basenames. It also isn't a fast-forward:staginghas one commit that branch lacks (82787ca, gradescope). None of that is needed to show the new landing, so this ports the public surface only — no backend changes, no migrations.What's in here
components/landing-v5/**andlib/landing/**— the landing engine,hero3d, drag simulation, and the Act sections(public)route group: the new/, plus/team,/wiki,/gallery,/news,/faq— the v5 navbar links to these and staging didn't have themcomponents/marketing/**andcomponents/companion/CompanionShellglobals.cssandapp/layout.tsx(OG/metadata),lib/deployGuard.tswith its matching test,lib/errorMessage.tsapi.tsgains onlysubscribeToNewsletter— staging's backend already mounts/api/newsletterOne deliberate divergence from upstream
marketing/SignInModalcalls the 3-argsetActiveUser. Upstream passes a 4th{ persist }option that exists only on that branch'sUserContext, whose wider change (+127/−21) also adds app-wide session recovery and shell-route redirects. Changing auth behaviour for every screen is out of scope for a landing port, so the option is dropped here and should come back in the same commit that brings thatUserContextover. The call site carries the same note.eslint-suppressions.jsongains an entry for the newcomponents/marketing/SignInModal.tsxpath; staging only suppressed that rule at the oldcomponents/SignInModal.tsxpath.Verification
tsc --noEmit— cleannext buildwith staging's build vars (DEPLOY_ENV/BACKEND_URL/NEXT_PUBLIC_API_URL/COOKIE_DOMAIN) — succeeds, emitting/team,/wiki,/gallery,/news,/faqeslint— clean except one pre-existing error:react-hooks/exhaustive-depsinsettings/GradescopeConnect.tsx:65, which arrives untouched fromorigin/staging(it is neither modified nor suppressed on that branch). CI will likely go red on it until it's fixed separately — deliberately not touched here.Deploy note
Per
wrangler.tomland ADR 0020, the staging Workers Build must setDEPLOY_ENV=stagingas a build variable, not just a runtime var — the/apirewrite andNEXT_PUBLIC_*are baked at build time. That omission is what previously put prod config on the staging worker.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements