Skip to content

Land dependency modernization stack: security bumps, TS6+Vite8, lazy catalogs, MUI 9, dep-shedding#166

Merged
rchalamala merged 22 commits into
mainfrom
devin/1781144811-audit-fix-bumps
Jun 12, 2026
Merged

Land dependency modernization stack: security bumps, TS6+Vite8, lazy catalogs, MUI 9, dep-shedding#166
rchalamala merged 22 commits into
mainfrom
devin/1781144811-audit-fix-bumps

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR now carries the entire 5-PR modernization stack (the children #165/#164/#163/#167 were merged downward into this branch):

Merge-resolution note: the old react-select options/firstLoad state workaround (and its async-catalog re-sync fixes) was removed entirely — the MUI Autocomplete derives options={courses} from the catalog on every render, so it handles async catalog loading without extra state; the search placeholder shows "Loading courses..." while the term catalog chunk loads.

npm run verify and npm audit (0 vulns) pass; all CI checks green.

Link to Devin session: https://app.devin.ai/sessions/3b5cf4353db148c09b27d04099b86357
Requested by: @rchalamala


Open in Devin Review

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 11, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
caltech-dev 015eb5e Commit Preview URL

Branch Preview URL
Jun 11 2026, 11:21 PM

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Test Results

Ran the production build (vite 7.3.5) served by npx wrangler dev (wrangler 4.98.0) locally and exercised the core scheduling flow end-to-end; also confirmed the vite dev server.

Deviations from the original request (intentional):

  • wrangler pinned to 4.98.0, not 4.99.0 — 4.99.0 (released 2026-06-09) has a regression where wrangler dev 404s all static assets (bisected: 4.90/4.95/4.98 serve dist/ fine; 4.99.0 404s even with a bare --assets ./dist). 4.98.0 already clears all undici/ws/miniflare advisories.
  • temporal-polyfill stays 0.3.0, not 0.3.2@schedule-x/calendar@4.6.0 pins exact peer temporal-polyfill@"0.3.0"; 0.3.2 fails npm install with ERESOLVE. No advisory affects 0.3.0.

Results:

  • It should serve the working app via wrangler dev 4.98.0 (production build, vite 7.3.5) — passed
  • It should add a course and render it on the weekly calendar (exercises temporal-polyfill/Schedule-X at runtime) — passed; console clean
  • It should serve the app via vite 7.3.5 dev server (npm start) — passed
Search results (wrangler dev :8787) Course added to calendar
Search CS 1 returns 35 results CS 1 MWF 2 PM blocks, 9 units
Shell evidence
  • npm run verify (oxlint + prettier check + tsc + vite build): passes
  • npm audit: found 0 vulnerabilities
  • npx wrangler dev → HTTP 200 on /; npm start → HTTP 200 on :3000

Devin session: https://app.devin.ai/sessions/44f717e165344a86812f49b427651bc6

devin-ai-integration Bot and others added 14 commits June 11, 2026 04:13
…ig-paths 6)

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
…ig-paths 6) (#165)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
…MB initial JS) (#164)

* perf: lazy-load term catalogs and split vendor chunks

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: no-op workspace mutations while term catalog is loading

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: log error when term catalog fails to load

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: sync catalog state before paint when the term changes

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* build: use function-form manualChunks for rolldown compatibility

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: re-sync search options after async catalog load

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: key search option re-sync on catalog identity

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
* chore: upgrade MUI to v9.1.0 (HelpOutline -> HelpOutlined icon rename)

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

* fix: use HelpOutlineOutlined to preserve original HelpOutline glyph

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
…animate (#167)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread src/css.d.ts
@@ -0,0 +1 @@
declare module "*.css";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: New css.d.ts declaration covers all CSS imports needed by the project

The new src/css.d.ts file declares *.css as a module. This is likely needed because Vite 8 (or TypeScript 6) changed how CSS imports are typed. The project imports CSS in four places (src/index.tsx, src/Planner.tsx × 3), all as side-effect-only imports (import "./foo.css"). The broad declare module "*.css" declaration covers all of these. This is a standard pattern for Vite projects.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — informational only. The declare module "*.css" shim covers the project's four side-effect CSS imports under the TypeScript 6 / Vite 8 toolchain from commit e6c7bf1, and npm run verify (tsc included) passes. No action needed.

Comment thread vite.config.ts Outdated
devin-ai-integration Bot and others added 3 commits June 11, 2026 22:48
…udit-fix-bumps

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title deps: fix npm audit vulnerabilities (vite 7.3.5, wrangler 4.98.0) Land dependency modernization stack: security bumps, TS6+Vite8, lazy catalogs, MUI 9, dep-shedding Jun 11, 2026
devin-ai-integration Bot and others added 2 commits June 11, 2026 23:00
…gression)

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 6 new potential issues.

Open in Devin Review

Comment thread .gitignore
Comment thread vite.config.ts Outdated
Comment thread src/Workspace.tsx
Comment on lines 429 to 430
selector: (item) => `${item.number} ${item.name}`,
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Fzf instance recreated on every render in WorkspaceSearch

The Fzf instance at src/Workspace.tsx:428-430 is created on every render of WorkspaceSearch, rebuilding its index over the full course list each time. This includes during every keystroke in the Autocomplete input. While functionally correct (each render gets a fresh fzf matching the current courses), wrapping it in useMemo(() => new Fzf(courses, {...}), [courses]) would avoid redundant index construction, especially since the courses array only changes when the catalog loads or the term switches.

(Refers to lines 428-430)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — correct that useMemo(() => new Fzf(courses, ...), [courses]) would avoid rebuilding the index per keystroke. Functionally fine today; noted as a possible perf follow-up rather than expanding this PR further.

Comment thread src/useAppState.ts
setWorkspaceIdx(storedIdx ? JSON.parse(storedIdx) : 0);
}, [realPath, defaultWorkspaces, setWorkspaces, setWorkspaceIdx]);

const catalogReady = Object.keys(indexedCourses).length > 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Async loading guards silently discard user interactions during catalog load

All state-modifying callbacks (addCourse, removeCourse, toggleCourse, toggleSectionLock, setCourses, nextArrangement, prevArrangement, updateAvailableTimes) now early-return when !catalogReady (e.g. src/useAppState.ts:144-146). This means if a user has courses from localStorage and interacts with them (toggling, locking, changing sections, navigating arrangements) before the async loadCourseIndex resolves, all those actions are silently ignored with no feedback. The WorkspaceSearch shows "Loading courses..." as placeholder, but the existing workspace entries appear fully interactive. Consider disabling the controls or showing a loading indicator over the workspace during the async load window.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid observation, but the window is narrow: the catalog chunk is a single local dynamic import (~135 kB gzip, cached after first load), so catalogReady flips within tens of ms in practice, and the search input already shows a "Loading courses..." placeholder. The guards exist to prevent corrupting persisted workspaces against an empty index, which is the worse failure mode. Leaving as-is for this PR; a loading overlay on the workspace list could be a follow-up if the gap proves noticeable.

Comment thread src/useAppState.ts
Comment on lines +69 to +89
useLayoutEffect(() => {
let cancelled = false;
const cached = getCachedCourseIndex(realPath);
if (cached) {
setIndexedCourses(cached);
return;
}
setIndexedCourses({});
loadCourseIndex(realPath)
.then((index) => {
if (!cancelled) {
setIndexedCourses(index);
}
})
.catch((error: unknown) => {
console.error(`Failed to load course data for ${realPath}:`, error);
});
return () => {
cancelled = true;
};
}, [realPath]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: useLayoutEffect used for async side effect is unusual but intentional

The useLayoutEffect at src/useAppState.ts:69-89 is used to kick off an async loadCourseIndex() call. Typically useLayoutEffect is reserved for synchronous DOM measurement/mutation. However, the intent here is to synchronously check the cache (getCachedCourseIndex) and set indexedCourses before paint when navigating between terms, so the calendar doesn't flash stale data. The async promise resolution happens after paint regardless. This is a valid pattern — the synchronous cache path benefits from useLayoutEffect while the async fallback is harmless. The cleanup function correctly cancels stale loads via the cancelled flag.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/courseData.ts
"/fa2027": () => import("./data/IndexedTotalFA2026-27.json"),
};

const courseIndexCache: { [key: string]: CourseIndex } = {};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Module-level courseIndexCache is a process-global singleton

The courseIndexCache at src/courseData.ts:21 is a module-level mutable object that persists across the entire application lifetime. This is fine for a client-side SPA, but if this module were ever imported in an SSR context, the cache would be shared across requests. Given this is a Vite + Cloudflare Workers (wrangler) project, verify that this module is only used on the client side. Currently it appears to be client-only based on the usage in useAppState.ts (a React hook), so this is safe.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…very (flatpickr removed)

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

Open in Devin Review

Comment thread src/Workspace.tsx
Comment thread src/Workspace.tsx
Comment on lines 215 to 222
value={
course.sectionId !== null
? course.courseData.sections.find(
? (course.courseData.sections.find(
(c) =>
c.number ===
course.courseData.sections[course.sectionId!].number,
)
) ?? null)
: null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: SectionDropdown value lookup is redundant but necessary for MUI Autocomplete referential identity

The value computation at lines 215-222 uses sections.find(...) to look up the section object by number, even though sections[course.sectionId!] would directly return it. This is intentional: MUI Autocomplete requires the value to be referentially identical to one of the options array elements. Since find searches the same sections array used as options, it guarantees the correct reference. The ?? null fallback handles the unlikely case where findIndex returns -1.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/courseData.ts
Comment on lines +33 to +34
if (!loader) {
return {};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: loadCourseIndex does not cache empty results for unknown terms

When loadCourseIndex is called with an unknown term (no matching loader), it returns {} without caching the result (src/courseData.ts:33-34). This means each call for an unknown term creates a new Promise and returns a new {} object. In the current code, useLayoutEffect only calls loadCourseIndex once per realPath change, so this isn't a performance issue. However, repeated navigation to an invalid term would repeatedly invoke the function. This is minor since there's no actual I/O for unknown terms.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread package.json
"vite": "^8.0.16",
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"wrangler": "4.98.0"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Wrangler version pinned without caret, unlike other dependencies

In package.json, wrangler changed from ^4.49.1 (allows minor/patch updates) to 4.98.0 (exact version). All other dependencies use ^ ranges. This inconsistency might be intentional (to pin a known-good version) but could lead to the project falling behind on wrangler patches. Worth confirming this was deliberate.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deliberate: wrangler 4.99.0 (latest at time of writing) has a regression where wrangler dev returns 404 for all static assets (bisected — 4.98.0 and earlier work). The exact pin prevents ^ from pulling in the broken version; it can be relaxed back to ^ once a fixed wrangler release ships.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

E2E test results — combined stack branch

Built this branch locally and served the production bundle via npx wrangler dev (wrangler 4.98.0), exercising the merge-resolution hot spots in the browser. All 4 tests passed.

Tests (4/4 passed)
  • Lazy catalog + MUI Autocomplete search (lazy-load × dep-shedding merge): passed. No IndexedTotal* chunk in initial load; IndexedTotalFA2026-27-*.js fetched on demand. fzf fuzzy search returns matches; selecting "Ae/APh/CE/ME 101 a - Fluid Mechanics" adds it to the workspace and renders MWF 10:00 events on the Schedule-X calendar.
  • Native time inputs (flatpickr removal): passed. Monday start is a native <input type="time">; setting 11:00 AM + Unlock All correctly excludes the MWF 10:00 section.
  • On-demand catalog for /wi2026: passed. IndexedTotalWI2025-26-*.js fetched on navigation; WI courses searchable.
  • Regression — workspace persistence: passed. Fluid Mechanics (9 units) restored from localStorage after reload.

Also at branch head: npm run verify passes, npm audit = 0 vulnerabilities, CI 6/6 green (incl. react-doctor).

T1: fzf search (fa2027) T1: course added, calendar events
search added
Preexisting bug found (NOT from this stack)

After unlocking a course and narrowing then re-widening the time window, "No arrangements found :(" persists and the section can't be re-selected. Root cause: src/parseTimes.ts builds section intervals on a 2018 reference date while src/appContext.ts availableTimes use 2025 reference dates, and src/scheduler.ts:87-88 compares absolute getTime() values — the window check always fails once the scheduler runs. Reproduced identically on a local build of main (came in with #160), so it predates this PR. Worth a follow-up fix.

This branch main (same behavior)
branch main
Notes
  • Initial "No options" for "cs 124" was a false alarm — CS 124 isn't offered in the fa2027 catalog; search works correctly.
  • wrangler is pinned to exactly 4.98.0 because 4.99.0/4.100.0 404 on assets under local wrangler dev.

Tested by Devin

…edule while catalog loads

Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread src/courseData.ts
Comment on lines +27 to +38
export async function loadCourseIndex(term: string): Promise<CourseIndex> {
const cached = courseIndexCache[term];
if (cached) {
return cached;
}
const loader = courseDataLoaders[term];
if (!loader) {
return {};
}
const module = await loader();
courseIndexCache[term] = module.default;
return module.default;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: loadCourseIndex has benign duplicate-call race but no deduplication

In src/courseData.ts:27-38, if loadCourseIndex is called concurrently for the same term (e.g., via React strict mode double-effects), both calls will miss the cache and independently call loader(). Both will resolve to the same module.default and both write it to the cache. This is harmless since the data is identical, but a pending-promise cache (storing the promise itself) would avoid the duplicate network/parse work. The useLayoutEffect in src/useAppState.ts:69-89 has a proper cancelled flag that handles this scenario correctly on the consumer side.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — the duplicate-call race is benign (same module record resolves for both callers; dynamic import() is deduped by the module loader, so there's no double network/parse in practice). A pending-promise cache would be a fine micro-optimization but isn't needed. Leaving as-is.

Comment thread src/Workspace.tsx
Comment on lines +200 to 205
const onChange = (newSection: Maybe<SectionData>) => {
course.sectionId =
newSection !== null
newSection != null
? course.courseData.sections.findIndex(
(s) => s.number === newSection.number,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: SectionDropdown.onChange directly mutates course.sectionId before calling addCourse

At src/Workspace.tsx:201, course.sectionId is mutated in-place (a direct state mutation) before state.addCourse(course) is called. This is a pre-existing pattern (existed before this PR), not introduced by the migration. It works because addCourse subsequently triggers a state update that persists the change. However, if addCourse returns early (e.g., due to the catalogReady guard), the mutation would persist on the object without a corresponding React state update, which could cause subtle inconsistencies. In practice this shouldn't happen since sections are only shown when courses are loaded.

(Refers to lines 200-208)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — pre-existing mutation pattern, not introduced by this PR. As noted, SectionDropdown only renders for courses already resolved from the catalog, so the catalogReady early-return path is effectively unreachable here. Leaving as-is; an immutable-update refactor could be a separate cleanup.

@rchalamala rchalamala merged commit 0068871 into main Jun 12, 2026
6 checks passed
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.

1 participant