Skip to content

Feat: Learning Record For Residents#1169

Open
gtonye wants to merge 22 commits into
UnlockedLabs:mainfrom
AlleyCorpNord:design/resident-homepage-on-main
Open

Feat: Learning Record For Residents#1169
gtonye wants to merge 22 commits into
UnlockedLabs:mainfrom
AlleyCorpNord:design/resident-homepage-on-main

Conversation

@gtonye

@gtonye gtonye commented Jun 11, 2026

Copy link
Copy Markdown

Pre-Submission PR Checklist

  • No debug/console/fmt.Println statements
  • Unnecessary development comments removed
  • All acceptance criteria verified
  • Functions according to ticket specifications
  • Tested manually where applicable
  • Branch rebased with latest main
  • No business logic exists within the database layer

Description of the change

This PR adds backend persistence for the Learning Record feature allowing residents to log

Backend

  • DB layer: get / create / update / delete learning record entries
  • HTTP handlers for 7 routes under /api/learning-record/
  • Feature flag for learning record

Frontend

  • Homepage widget to prompt and promote the functionalityto resident
  • Page to manage achievements

How to test

  1. As a superadmin, go to Feature Control and enable the Learning Record feature
  2. As a resident, verify that the Learning Record is visible
  3. Create a new achievement and complete all steps through to submission — verify the entry persists after a page reload
  4. Draft edge case — start filling in an achievement but do not submit; reload the page and verify the draft is restored and flagged as incomplete

Screenshot(s)

a-learning-record-feature-flag b-learning-record-resident-home

Additional context

Technical design decision — column-per-question schema

The questionnaire is stored in a dedicated table with each question as its own column, rather than a single JSONB column containing the full response payload. This prioritizes maintainability through migrations:

  • Adding a new question means adding a column, which is explicit, trackable, and straightforward to roll back
  • JSONB would require application-level logic to manage schema evolution inside the blob and coordinate data migrations accordingly
  • Column-level constraints, indexing, and query filtering are all simpler with discrete columns

gtonye and others added 10 commits June 10, 2026 15:38
- Integrated funnel functionality in DigitalTranscriptEntryPage, allowing for a streamlined user experience when adding achievements.
- Added back navigation handling that commits session rows before navigating back.
- Updated DigitalTranscriptWysiwygEntry to support single-row sessions for funnel mode.
- Improved UI components to conditionally render based on funnel state, enhancing usability.
- Introduced new helper functions for managing session states and entries.
…funnel support

- Refactored AchievementForm and AchievementFormMetadata to support funnel step navigation and error handling.
- Updated AchievementRow to conditionally render based on funnel state, integrating save and cancel functionalities.
- Enhanced AchievementsRecordPreview to include download capabilities for funnel mode.
- Introduced new props and handlers for managing funnel interactions across digital transcript components.
- Improved UI consistency and user experience in the digital transcript entry flow.
…I improvements

- Updated AchievementForm and AchievementRow to integrate funnel navigation and save functionalities.
- Enhanced DigitalTranscriptEntryPage with alert dialogs for unsaved changes and streamlined navigation.
- Improved UI consistency across digital transcript components, including the addition of new props for funnel handling.
- Refactored ConfidenceSegmentedControl and LearningRecordDocument to support funnel-specific logic and display.
- Introduced new badge components for better visual feedback on progress within the funnel.
…siveness

- Changed table class to 'table-fixed' for better layout control.
- Adjusted column widths for 'Completed', 'Questions answered', and 'Added on' headers to ensure consistent display across different screen sizes.
- Enhanced cell alignment for better visual consistency in the SavedEntriesSection.
…rovements

- Added extra_hosts configuration in docker-compose for improved local development.
- Updated Nginx configuration to use dynamic upstream variables for API and frontend proxying.
- Enhanced ConfirmDialog component to accept custom button class names for better styling flexibility.
- Refactored useTranscriptDraft hook to include additional fields for transcript entries.
- Improved AchievementForm and related components with new props for better funnel integration and UI consistency.
- Introduced sorting functionality in DigitalTranscriptHome for better user experience when viewing achievements.
Give residents a clearer home experience that surfaces in-progress achievements, reduces duplicate resume CTAs, and aligns copy and title display across the learning record flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ort to main

- Add missing digital-transcript files dropped during cherry-pick
  (digitalTranscriptRoutes, learningRecordPrototypes, learningRecordResidentName,
  TranscriptResumePreview, AchievementFormCategories, AchievementFormCategoryCard,
  AchievementFormSectionHeader, AchievementPreview, TopSkillsTagField)
- Fix import paths in ResidentHomeDashboard (useTourContext, targetToStepIndexMap)
- Add missing imports for DigitalTranscriptHome/EntryPage in app-routes.tsx
- Remove demo calendar data fallback in favour of real API data
- Wrap .card-block in @layer components so Tailwind utilities can override bg colour
- Restore nginx to Docker container names (server/frontend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ge persistence

Replace all localStorage reads/writes for entries and drafts with API calls
to the learning-record endpoints. The hook now fetches on mount, autosaves
drafts via debounced PUT, and persists committed entries through POST/PUT/DELETE.
Session UI state (expanded row, etc.) remains in React state only.
Introduces LearningRecordAccess feature flag end-to-end: DB migration, backend route gating via featureRoute, frontend enum/auth/sidebar/admin toggle, and dedicated learning-record-routes file with routes moved out of knowledge-routes.
@gtonye gtonye requested a review from a team as a code owner June 11, 2026 05:27
@gtonye gtonye requested review from carddev81 and removed request for a team June 11, 2026 05:27
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added Learning Record feature for documenting completed programs and achievements
    • Added ability to save work as drafts and publish completed entries
    • Added PDF export for saving and sharing learning records
    • Added automatic draft autosaving
    • Added in-progress entry reminder banner on homepage
    • Added dedicated Learning Record navigation and dashboard

Walkthrough

This PR adds learning record database storage, backend APIs, frontend routes and feature gating, transcript editing and preview flows, PDF export, and a resident home dashboard that surfaces learning record state alongside related content and reminders.

Changes

Learning record rollout

Layer / File(s) Summary
Backend schema, model, and API endpoints
backend/migrations/00069_create_learning_record_tables.sql, backend/src/models/learning_record.go, backend/src/database/learning_record.go, backend/src/handlers/learning_record_handler.go, backend/src/models/feature_flags.go, backend/src/models/users.go, backend/src/handlers/server.go
Adds the learning record table, backend model, entry and draft persistence methods, delete history logging, feature flag registration, and API route wiring.
Frontend API and access wiring
frontend/src/api/learningRecord.ts, frontend/src/routes/learning-record-routes.tsx, frontend/src/routes/app-routes.tsx, frontend/src/routes/index.tsx, frontend/src/routes/knowledge-routes.tsx, frontend/src/auth/useAuth.ts, frontend/src/components/navigation/Sidebar.tsx, frontend/src/layouts/AuthenticatedLayout.tsx, frontend/src/pages/admin/FeatureControl.tsx, frontend/src/types/user.ts
Adds the learning record API client, authenticated routes, feature checks, sidebar and layout handling, and admin feature control UI.
Digital transcript editing, state, and navigation
frontend/src/types/digital-transcript.ts, frontend/src/hooks/useTranscriptDraft.ts, frontend/src/pages/student/digital-transcript/*
Adds transcript draft and entry types, local storage migration and session helpers, reflection configuration, sorting and title helpers, editing forms, transcript home, entry editor, and editing session behavior for funnel and categories variants.
Learning record preview and export surfaces
frontend/src/pages/student/digital-transcript/LearningRecord*.tsx, frontend/src/pages/student/digital-transcript/AchievementPreview.tsx, frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx, frontend/src/pages/student/digital-transcript/ViewAllAchievementsSheet.tsx, frontend/src/components/learning-record/PrintShareHelpLink.tsx, frontend/src/data/*faq*, frontend/src/components/shared/ConfirmDialog.tsx, frontend/src/components/ui/alert-dialog.tsx
Adds document rendering, preview panes, export preview containers, all-achievements export UI, learning record FAQ copy, and supporting dialog updates.
Resident home and dashboard composition
frontend/src/pages/student/ResidentHome.tsx, frontend/src/pages/student/ResidentHomeDashboard.tsx, frontend/src/loaders/routeLoaders.ts, frontend/src/components/dashboard/*
Refactors the resident home route into a dashboard wrapper, adds learning record reminders and achievements panels, adds discover and upcoming-session sections, and expands loader data with featured libraries.
PDF capture and export engine
frontend/package.json, frontend/src/utils/downloadLearningRecordPdf.ts
Adds DOM capture and PDF generation utilities plus the runtime packages used for learning record export.

Ancillary resident and local environment updates

Layer / File(s) Summary
Resident courses page and minor app adjustments
frontend/src/pages/learning/ResidentOnlineCourses.tsx, frontend/src/components/UnlockEdTour.tsx, frontend/src/styles/globals.css, docker-compose.yml
Adds a resident online courses page, updates one tour target selector, moves .card-block into Tailwind components, and adds a reverse-proxy host gateway mapping.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 29

🤖 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 `@backend/migrations/00073_add_learning_record_feature_flag.sql`:
- Line 10: The DELETE statement violates SQLFluff LT14; reformat the statement
so the DELETE keyword and the WHERE clause are on separate lines to satisfy the
linter: locate the DELETE targeting public.feature_flags where name =
'learning_record' and split it into a DELETE FROM public.feature_flags line
followed by a separate WHERE name = 'learning_record' line so the clause is on
its own line.

In `@backend/src/database/learning_record.go`:
- Around line 29-31: The update/delete DB calls that use result :=
db.Model(entry)... (e.g., the update path where result is assigned and similar
blocks around lines handling deletion and bulk operations) only check
result.Error; change these to also check result.RowsAffected and treat
RowsAffected == 0 as a not-found/unauthorized outcome: after the DB call (the
result variable from db.Model(entry).Where(...).Updates(entry) and the
corresponding delete calls), if result.Error != nil return the error, else if
result.RowsAffected == 0 return a not-found/unauthorized error (same error
type/status your handlers use for missing ownership), so that non-matching
ownership filters do not return success.
- Line 31: The code is using GORM's Updates(entry) which omits zero-value fields
(so clearing fields fails); change the call that currently uses Updates(entry)
in learning_record.go to use UpdateColumns(entry) on the same model/DB chain (or
alternatively construct a map[string]interface{} and pass that to Updates) so
zero/empty values are written; update the call site where Updates(entry) is
invoked to use UpdateColumns(entry) (or a map) to ensure zero-value fields are
persisted.

In `@backend/src/handlers/learning_record_handler.go`:
- Around line 47-50: The code currently parses path IDs with strconv.Atoi (via
r.PathValue("id")) and then allows negative values to be cast to uint, producing
invalid large IDs; update both parsing sites (the blocks using strconv.Atoi and
newInvalidIdServiceError) to reject non-positive IDs by checking if id <= 0 and
returning newInvalidIdServiceError(errOrCustom, "entry ID") (or a clear
invalid-id error) before casting to uint; ensure the uint conversion only occurs
after the positive check so functions using the resulting uint get a valid ID.
- Around line 33-39: The create and update handlers are allowing clients to set
the draft flag; force published state by overriding the entry's draft field
before persisting. In handleCreateLearningRecordEntry (before calling
srv.Db.CreateLearningRecordEntry) set the LearningRecordEntry's IsDraft (or
is_draft) to false/unset so clients cannot submit drafts to the published
endpoint; do the same in the corresponding update handler (the code path that
calls srv.Db.UpdateLearningRecordEntry / UpdateLearningRecordEntry) to ensure
incoming payloads cannot toggle entries back to draft.

In `@backend/src/models/learning_record.go`:
- Around line 20-31: The Scan method on StringSlice currently swallows
unexpected DB value types by defaulting to "[]"; update StringSlice.Scan to
return a descriptive error in the default branch (including the concrete type of
value) instead of silently unmarshaling "[]", and handle nil explicitly if
needed (either treat nil as empty array or return an error) so callers see type
mismatches; reference the StringSlice.Scan function and its switch on
value.(type) to locate and change the default behavior to return
fmt.Errorf("unexpected type for StringSlice: %T", value) (or similar).

In `@frontend/package.json`:
- Around line 49-52: Remove the unused html2pdf.js dependency from
frontend/package.json: confirm there are no runtime imports (e.g., check
frontend/src/utils/downloadLearningRecordPdf.ts uses only html2canvas and
jspdf), remove the "html2pdf.js" entry from the dependencies, then run the
package manager (yarn/npm install) to update node_modules and regenerate the
lockfile; optionally run a repo-wide search for "html2pdf" to verify nothing
else references it before committing.

In `@frontend/src/api/learningRecord.ts`:
- Around line 116-177: The API functions (apiGetEntries, apiCreateEntry,
apiUpdateEntry, apiDeleteEntry, apiGetDraft, apiUpsertDraft, apiDeleteDraft)
currently swallow failures; add error logging/telemetry whenever resp.success is
false or resp.type/data are not as expected by calling console.error or your
error-tracking helper with a clear message plus the full resp and relevant
identifiers (e.g., backendId, clientId, entry.id) so failures are recorded; for
apiGetDraft also log when b or b.client_id is missing and include b.updated_at
when present; keep behavior otherwise (return null/false/empty) but ensure every
early-return on failed response logs diagnostic details.
- Line 46: Validate the incoming challenge_toggle before casting: replace the
unsafe cast used for q4Toggle by checking that (b.challenge_toggle) is strictly
one of the allowed TranscriptQ4Toggle values ('yes' or 'notReally') or null, and
only then assign it to q4Toggle; if the value is anything else, set q4Toggle to
null (or another safe default). Implement a small type-guard function (e.g.,
isTranscriptQ4Toggle) and use it when mapping b.challenge_toggle to q4Toggle to
prevent silently accepting invalid backend values.
- Line 161: The fallback to new Date().toISOString() when converting a backend
record (variable b) to the frontend model in the call to toDraftFrontend(b,
b.updated_at ?? new Date().toISOString()) is misleading; instead pass a
null/undefined sentinel so the frontend knows updated_at is unknown (or log the
missing value), e.g. call toDraftFrontend with b.updated_at as-is (no
current-time fallback) or explicitly pass null and update
toDraftFrontend/signature handling if needed to accept nullable updated_at;
locate the call in frontend/src/api/learningRecord.ts and adjust the call site
and any dependent handling in toDraftFrontend to treat missing updated_at as
unknown rather than current time.

In `@frontend/src/components/dashboard/ContinueLearningSection.tsx`:
- Around line 78-91: The mapping currently hardcodes statusLabel from the array
index and sets progressPercent to 0; update the map to derive progressPercent
from the actual item field (e.g., item.progress, item.progress_percent, or
item.user_progress) and compute statusLabel from that value (e.g., 0 -> "Not
started yet", 0<progress<100 -> "In progress" or "Your progress", 100 ->
"Completed"); if OpenContentItem contains no progress field, return no
progressPercent (undefined/null) and do not render progress UI elsewhere; keep
resolveContentHref usage and the existing id/title/contentType fields unchanged
and add defensive checks for missing progress properties.

In `@frontend/src/components/dashboard/DiscoverContentSection.tsx`:
- Around line 167-182: The handleClick function lacks error handling around the
helpful link tracking API call (API.put in the block guarded by
item.helpfulLinkId); wrap that await API.put<{ url: string }, object>(...) call
in a try-catch, log the caught error (use existing logger or console.error) and
still fall back to item.href if request/response fails, then proceed to open the
link (window.open) or navigate(item.href); optionally, call the existing
toast/notification utility in the catch to surface persistent failures to the
user.

In `@frontend/src/components/dashboard/UpcomingClassSessionCard.tsx`:
- Around line 103-108: The inProgress check uses UTC `now` against raw session
times causing timezone mismatch; update it to compare times in the same timezone
as the converted start/end (use the already computed `start` and `end` from
toZonedTime or convert `now` with `toZonedTime(now, timezone)`) so the
`inProgress` boolean is determined by comparing like-for-like zoned Date objects
(refer to the `toZonedTime` usage and the `inProgress` variable that reads
`nextSession.start`/`end`).

In `@frontend/src/hooks/useTranscriptDraft.ts`:
- Around line 73-80: The autosave currently fire-and-forgets apiUpsertDraft in
useTranscriptDraft (using hydrated, draft and DRAFT_AUTOSAVE_MS) so failures are
silent; add an autosave error state (e.g., autosaveError via useState) and
replace the void call with a promise chain/async handling that sets
autosaveError on .catch or when the API indicates failure and clears it on
success, log the error for debugging, and expose autosaveError from
useTranscriptDraft so the UI can surface a warning to users when autosave fails.
- Line 43: The autosave debounce constant DRAFT_AUTOSAVE_MS in
useTranscriptDraft.ts is too low (400ms) and should be increased to reduce API
traffic; update the DRAFT_AUTOSAVE_MS value to a more conservative interval
(recommend 1000–2000ms, e.g., 1500) and ensure any debounce logic that
references DRAFT_AUTOSAVE_MS (in the useTranscriptDraft hook) continues to use
the constant so autosave timing is centralized, then run related tests or manual
verification to confirm autosave behavior.
- Around line 96-102: The startFreshDraft function currently fires
apiDeleteDraft(prev.id) without awaiting it causing a race; make startFreshDraft
async and await apiDeleteDraft(prev.id) (inside a try/catch) before creating the
fresh draft with createEmptyDraft and calling setDraft, and on delete failure
log or surface the error and decide whether to abort creating the new draft;
update any callers to handle the now-async startFreshDraft accordingly and keep
using draftRef/current references (draftRef, apiDeleteDraft, createEmptyDraft,
setDraft) to locate the changes.
- Around line 104-126: upsertCommittedEntry currently calls
apiUpdateEntry/apiCreateEntry without error handling, causing silent failures
and data loss; wrap the async API calls in try/catch, only mutate
entryBackendIds.current, call setEntries, and invoke dispatchEntrySessionUpdated
on successful API responses, and on catch show a user-facing error (e.g.,
toast/notification) and keep the local state untouched (or roll back any
optimistic changes); ensure references to apiUpdateEntry, apiCreateEntry,
entryBackendIds.current, setEntries, and dispatchEntrySessionUpdated are updated
accordingly and maintain trimming via TOP_SKILLS_MAX before sending.

In `@frontend/src/loaders/routeLoaders.ts`:
- Around line 70-80: The mapping that builds featuredLibraries unnecessarily
uses nullish coalescing for the description field (lib.description ??
undefined); update the mapping inside the featuredLibraries assignment to assign
description directly from lib.description (i.e., description: lib.description)
since OpenContentItem already permits undefined, removing the redundant ??
undefined in the mapping for clarity.

In `@frontend/src/pages/learning/ResidentOnlineCourses.tsx`:
- Around line 85-94: Compute a single, clamped progress number and reuse it for
both the label and the bar width instead of calling
Math.floor(course.course_progress) twice; e.g., create a local variable (e.g.,
progressRaw = Number(course.course_progress) || 0; progressClamped = Math.max(0,
Math.min(100, Math.floor(progressRaw)))) and then render {progressClamped}% for
the label and style={{ width: `${progressClamped}%` }} for the bar (use the same
variable in the JSX where course.course_progress is currently used).

In `@frontend/src/pages/student/digital-transcript/AchievementRow.tsx`:
- Around line 95-156: In AchievementRow (the button span, the chevron container
span, and the progress bar inner span) replace the hardcoded color '`#556830`'
with theme/tailwind tokens (e.g., use a text token like text-primary or
text-primary-600 for the hover text and a background token like bg-primary-600
for the progress filled state) by updating the cn(...) class strings where
'`#556830`' appears (the title group-hover, the chevron group-hover, and the
complete ? progress class). Ensure you pick the appropriate dark/light variants
if needed (e.g., dark:bg-primary-600) and keep the existing conditional logic
(complete ? ... : ...) and aria attributes unchanged.

In
`@frontend/src/pages/student/digital-transcript/DigitalTranscriptEntryPage.tsx`:
- Around line 10-13: The code calls readLearningRecordExportRows without
importing it and with the wrong signature; import readLearningRecordExportRows
from the transcriptEntrySessionStorage module and call it with the required
TranscriptEntry[] argument (the committed entries array you already manage in
DigitalTranscriptEntryPage, e.g., committedEntries or the state/prop that holds
saved TranscriptEntry items) instead of zero args; ensure the import name
matches the exported symbol and that the caller passes the correct variable of
type TranscriptEntry[] when invoking readLearningRecordExportRows.

In `@frontend/src/pages/student/digital-transcript/DigitalTranscriptHome.tsx`:
- Around line 303-349: The TableCell elements are being wrapped by Button/Link
which creates invalid table markup; change the structure so TableCell elements
are direct children of TableRow and do not have Button or Link as their parent.
Move the Link (using editHref) inside the appropriate TableCell content (e.g.,
wrap the title span or all inner content) or add an onClick/onKeyDown on
TableRow to navigate to editHref while keeping TableCell nodes intact; update
TableRow (and getEntryDisplayTitle usage) to provide keyboard accessibility
(handle Enter/Space) and ensure aria attributes/role are set if you make the row
interactive.

In `@frontend/src/pages/student/digital-transcript/ReflectionStepField.tsx`:
- Around line 53-56: The conditional in handleChange is redundant; always call
onChange with next.slice(0, nudge.maxLength) which will return the original
string when shorter and truncate when longer. Update the handleChange function
to remove the if-else and invoke onChange(next.slice(0, nudge.maxLength)) so you
only reference handleChange, onChange, and nudge.maxLength.
- Line 121: The onChange inline handler calls v.trim() twice; compute const
trimmed = v.trim() once and use it in the condition and when setting topSkills
to avoid duplicate work and improve readability (update the onChange handler
that currently sets topSkills: v.trim() ? [v.trim()] : [] to use the single
trimmed variable).

In `@frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx`:
- Line 91: Update the user-facing heading in the TranscriptResumePreview
component: replace the incorrect phrase "Achievements logs of {name}" with a
grammatically correct variant such as "Achievement record for {name}" or
"Achievement records for {name}" (use the chosen wording wherever the heading
string is rendered and ensure it still interpolates the name variable).
- Around line 76-79: The className string in TranscriptResumePreview's JSX uses
legacy Tailwind important-prefix syntax ('!px-0 !pt-0') when computing classes
via cn; update this to Tailwind v4 suffix important syntax by replacing those
tokens with 'px-0! pt-0!' so the conditional branch that uses isPaper continues
to apply the same important styles using the new suffix form inside the cn call.

In `@frontend/src/pages/student/ResidentHome.tsx`:
- Line 106: Remove the redundant null-coalescing when passing featuredLibraries
to ResidentHomeDashboard: since featuredLibraries is obtained from
useLoaderData() and the loader (routeLoaders.ts) always includes
featuredLibraries (defaulting to []), change the prop from
featuredLibraries={featuredLibraries ?? []} to simply
featuredLibraries={featuredLibraries}; update the JSX in ResidentHome (the prop
passed to ResidentHomeDashboard) accordingly and run tests/lint to ensure no
further references rely on the fallback.

In `@frontend/src/pages/student/ResidentHomeDashboard.tsx`:
- Around line 349-351: The useMemo for incompleteEntry currently includes an
unnecessary dependency hasIncompleteEntry; update the dependency array for the
useMemo that defines incompleteEntry to only include the actual inputs used by
findIncompleteAchievementEntry — learningRecordEntries, entrySessionTick, and
learningRecordFormVariant — so remove hasIncompleteEntry from the dependencies
of the useMemo (the useMemo call creating incompleteEntry that invokes
findIncompleteAchievementEntry).

In `@frontend/src/routes/learning-record-routes.tsx`:
- Around line 12-35: The routes for 'learning-record-funnel' and
'learning-record-categories' both render the same components
(DigitalTranscriptHome and DigitalTranscriptEntryPage); either consolidate them
into a single route group or add an explanatory comment; specifically, in the
route definitions where path: 'learning-record-funnel' and path:
'learning-record-categories' reference DigitalTranscriptHome and
DigitalTranscriptEntryPage, decide whether to merge into one path (removing the
duplicate group) or keep both and add a clear comment above each route
explaining why both exist (e.g., A/B/variant testing or future divergence) so
future readers understand the intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a35cf888-81e7-4616-9b9b-3bf25e21ff12

📥 Commits

Reviewing files that changed from the base of the PR and between 6c74d05 and 552410b.

⛔ Files ignored due to path filters (1)
  • frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (70)
  • backend/migrations/00072_create_learning_record_tables.sql
  • backend/migrations/00073_add_learning_record_feature_flag.sql
  • backend/src/database/learning_record.go
  • backend/src/handlers/learning_record_handler.go
  • backend/src/handlers/server.go
  • backend/src/models/feature_flags.go
  • backend/src/models/learning_record.go
  • config/dev.nginx.conf
  • docker-compose.yml
  • frontend/package.json
  • frontend/src/api/learningRecord.ts
  • frontend/src/auth/useAuth.ts
  • frontend/src/components/UnlockEdTour.tsx
  • frontend/src/components/dashboard/ContinueLearningSection.tsx
  • frontend/src/components/dashboard/DiscoverContentSection.tsx
  • frontend/src/components/dashboard/IncompleteEntryReminder.tsx
  • frontend/src/components/dashboard/UpcomingClassSessionCard.tsx
  • frontend/src/components/learning-record/PrintShareHelpLink.tsx
  • frontend/src/components/navigation/Sidebar.tsx
  • frontend/src/components/shared/ConfirmDialog.tsx
  • frontend/src/components/ui/alert-dialog.tsx
  • frontend/src/data/faqData.ts
  • frontend/src/data/learningRecordResidentCopy.ts
  • frontend/src/hooks/useTranscriptDraft.ts
  • frontend/src/loaders/routeLoaders.ts
  • frontend/src/pages/admin/FeatureControl.tsx
  • frontend/src/pages/learning/ResidentOnlineCourses.tsx
  • frontend/src/pages/student/ResidentHome.tsx
  • frontend/src/pages/student/ResidentHomeDashboard.tsx
  • frontend/src/pages/student/digital-transcript/AchievementForm.tsx
  • frontend/src/pages/student/digital-transcript/AchievementFormActions.tsx
  • frontend/src/pages/student/digital-transcript/AchievementFormCategories.tsx
  • frontend/src/pages/student/digital-transcript/AchievementFormCategoryCard.tsx
  • frontend/src/pages/student/digital-transcript/AchievementFormMetadata.tsx
  • frontend/src/pages/student/digital-transcript/AchievementFormSectionHeader.tsx
  • frontend/src/pages/student/digital-transcript/AchievementPreview.tsx
  • frontend/src/pages/student/digital-transcript/AchievementRow.tsx
  • frontend/src/pages/student/digital-transcript/AchievementsRecordPreview.tsx
  • frontend/src/pages/student/digital-transcript/AddAchievementRow.tsx
  • frontend/src/pages/student/digital-transcript/ConfidenceSegmentedControl.tsx
  • frontend/src/pages/student/digital-transcript/DigitalTranscriptEntryPage.tsx
  • frontend/src/pages/student/digital-transcript/DigitalTranscriptHome.tsx
  • frontend/src/pages/student/digital-transcript/DigitalTranscriptShell.tsx
  • frontend/src/pages/student/digital-transcript/DigitalTranscriptWysiwygEntry.tsx
  • frontend/src/pages/student/digital-transcript/LearningRecordDocument.tsx
  • frontend/src/pages/student/digital-transcript/LearningRecordDocumentNarrative.tsx
  • frontend/src/pages/student/digital-transcript/LearningRecordExportContent.tsx
  • frontend/src/pages/student/digital-transcript/ReflectionStepField.tsx
  • frontend/src/pages/student/digital-transcript/ReflectionTextField.tsx
  • frontend/src/pages/student/digital-transcript/TopSkillsTagField.tsx
  • frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx
  • frontend/src/pages/student/digital-transcript/ViewAllAchievementsSheet.tsx
  • frontend/src/pages/student/digital-transcript/confidenceLevelVisual.ts
  • frontend/src/pages/student/digital-transcript/digitalTranscriptRoutes.ts
  • frontend/src/pages/student/digital-transcript/entryTitleDisplay.ts
  • frontend/src/pages/student/digital-transcript/learningRecordButtons.ts
  • frontend/src/pages/student/digital-transcript/learningRecordDocumentModel.ts
  • frontend/src/pages/student/digital-transcript/learningRecordPrototypes.ts
  • frontend/src/pages/student/digital-transcript/learningRecordResidentName.ts
  • frontend/src/pages/student/digital-transcript/learningRecordTableSort.ts
  • frontend/src/pages/student/digital-transcript/transcriptEntrySessionStorage.ts
  • frontend/src/pages/student/digital-transcript/transcriptReflectionConfig.ts
  • frontend/src/routes/app-routes.tsx
  • frontend/src/routes/index.tsx
  • frontend/src/routes/knowledge-routes.tsx
  • frontend/src/routes/learning-record-routes.tsx
  • frontend/src/styles/globals.css
  • frontend/src/types/digital-transcript.ts
  • frontend/src/types/user.ts
  • frontend/src/utils/downloadLearningRecordPdf.ts
💤 Files with no reviewable changes (1)
  • frontend/src/routes/knowledge-routes.tsx

Comment thread backend/migrations/00073_add_learning_record_feature_flag.sql Outdated
Comment on lines +29 to +31
result := db.Model(entry).
Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
Updates(entry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing match checks allow false-success updates/deletes.

Line 29-Line 31 (and similarly Line 39-Line 40, Line 83-Line 84) only check result.Error; when no row matches ownership filters, handlers still return success. Please treat RowsAffected == 0 as not-found/unauthorized for these mutations.

Suggested fix
 func (db *DB) UpdateLearningRecordEntry(entry *models.LearningRecordEntry) error {
-    result := db.Model(entry).
+    result := db.Model(&models.LearningRecordEntry{}).
         Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
         Updates(entry)
     if result.Error != nil {
         return newUpdateDBError(result.Error, "learning_record_entries")
     }
+    if result.RowsAffected == 0 {
+        return newUpdateDBError(gorm.ErrRecordNotFound, "learning_record_entries")
+    }
     return nil
 }
🤖 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 `@backend/src/database/learning_record.go` around lines 29 - 31, The
update/delete DB calls that use result := db.Model(entry)... (e.g., the update
path where result is assigned and similar blocks around lines handling deletion
and bulk operations) only check result.Error; change these to also check
result.RowsAffected and treat RowsAffected == 0 as a not-found/unauthorized
outcome: after the DB call (the result variable from
db.Model(entry).Where(...).Updates(entry) and the corresponding delete calls),
if result.Error != nil return the error, else if result.RowsAffected == 0 return
a not-found/unauthorized error (same error type/status your handlers use for
missing ownership), so that non-matching ownership filters do not return
success.

func (db *DB) UpdateLearningRecordEntry(entry *models.LearningRecordEntry) error {
result := db.Model(entry).
Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
Updates(entry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Struct Updates(entry) drops zero-value writes.

Line 31 uses struct updates, so empty strings/zero numbers/false values are skipped by GORM. This breaks legitimate “clear this field” edits in learning record entries.

Suggested fix
-    result := db.Model(&models.LearningRecordEntry{}).
-        Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
-        Updates(entry)
+    result := db.Model(&models.LearningRecordEntry{}).
+        Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
+        Select("*").
+        Omit("id", "user_id", "created_at").
+        Updates(entry)
📝 Committable suggestion

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

Suggested change
Updates(entry)
result := db.Model(&models.LearningRecordEntry{}).
Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
Select("*").
Omit("id", "user_id", "created_at").
Updates(entry)
🤖 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 `@backend/src/database/learning_record.go` at line 31, The code is using GORM's
Updates(entry) which omits zero-value fields (so clearing fields fails); change
the call that currently uses Updates(entry) in learning_record.go to use
UpdateColumns(entry) on the same model/DB chain (or alternatively construct a
map[string]interface{} and pass that to Updates) so zero/empty values are
written; update the call site where Updates(entry) is invoked to use
UpdateColumns(entry) (or a map) to ensure zero-value fields are persisted.

Comment on lines +33 to +39
func (srv *Server) handleCreateLearningRecordEntry(w http.ResponseWriter, r *http.Request, log sLog) error {
var entry models.LearningRecordEntry
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
return newJSONReqBodyServiceError(err)
}
entry.UserID = r.Context().Value(ClaimsKey).(*Claims).UserID
if err := srv.Db.CreateLearningRecordEntry(&entry); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

/entries handlers should force published state.

Line 33-Line 39 and Line 55-Line 57 let clients control is_draft. That allows draft-state payloads through published-entry endpoints, which breaks API semantics and can hide entries from the list query.

Suggested fix
 func (srv *Server) handleCreateLearningRecordEntry(w http.ResponseWriter, r *http.Request, log sLog) error {
     var entry models.LearningRecordEntry
     if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
         return newJSONReqBodyServiceError(err)
     }
     entry.UserID = r.Context().Value(ClaimsKey).(*Claims).UserID
+    entry.IsDraft = false
 func (srv *Server) handleUpdateLearningRecordEntry(w http.ResponseWriter, r *http.Request, log sLog) error {
@@
     entry.ID = uint(id)
     entry.UserID = r.Context().Value(ClaimsKey).(*Claims).UserID
+    entry.IsDraft = false

Also applies to: 55-57

🤖 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 `@backend/src/handlers/learning_record_handler.go` around lines 33 - 39, The
create and update handlers are allowing clients to set the draft flag; force
published state by overriding the entry's draft field before persisting. In
handleCreateLearningRecordEntry (before calling
srv.Db.CreateLearningRecordEntry) set the LearningRecordEntry's IsDraft (or
is_draft) to false/unset so clients cannot submit drafts to the published
endpoint; do the same in the corresponding update handler (the code path that
calls srv.Db.UpdateLearningRecordEntry / UpdateLearningRecordEntry) to ensure
incoming payloads cannot toggle entries back to draft.

Comment on lines +47 to +50
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
return newInvalidIdServiceError(err, "entry ID")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-positive path IDs before uint conversion.

Line 47-Line 50 and Line 66-Line 69 accept negative integers, then cast to uint, producing invalid large IDs. Validate id > 0 before using it.

Suggested fix
 id, err := strconv.Atoi(r.PathValue("id"))
 if err != nil {
     return newInvalidIdServiceError(err, "entry ID")
 }
+if id <= 0 {
+    return newBadRequestServiceError(nil, "entry ID must be a positive integer")
+}

Also applies to: 66-69

🤖 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 `@backend/src/handlers/learning_record_handler.go` around lines 47 - 50, The
code currently parses path IDs with strconv.Atoi (via r.PathValue("id")) and
then allows negative values to be cast to uint, producing invalid large IDs;
update both parsing sites (the blocks using strconv.Atoi and
newInvalidIdServiceError) to reject non-positive IDs by checking if id <= 0 and
returning newInvalidIdServiceError(errOrCustom, "entry ID") (or a clear
invalid-id error) before casting to uint; ensure the uint conversion only occurs
after the positive check so functions using the resulting uint get a valid ID.

Comment on lines +76 to +79
className={cn(
'space-y-2 border-b border-gray-100 pb-5 dark:border-border',
isPaper && '!px-0 !pt-0 pb-4'
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find tailwind version info
rg -n --hidden -S '"tailwindcss"|' package.json **/package.json 2>/dev/null || true
rg -n --hidden -S 'tailwindcss@' **/package-lock.json **/pnpm-lock.yaml **/yarn.lock 2>/dev/null || true
rg -n --hidden -S 'tailwindcss' frontend/package.json frontend/**/package.json 2>/dev/null || true

# Check the file in question
sed -n '60,95p' frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx

# Search for other usages of the important modifier syntax
rg -n --hidden -S "'!px-0|!px-0|!pt-0|px-0!|pt-0!" frontend/src 2>/dev/null || true

Repository: UnlockedLabs/UnlockEdv2

Length of output: 7656


🌐 Web query:

Tailwind v4 important modifier syntax px-0! vs !px-0

💡 Result:

In Tailwind CSS v4, the correct syntax for the important modifier is to place the exclamation mark at the end of the utility class (e.g., px-0!) [1][2][3]. This change brings Tailwind's syntax in line with native CSS, where the !important rule is placed after the declaration [2]. While the older syntax of placing the exclamation mark at the beginning (e.g., !px-0) is supported in v4 for backwards compatibility, the trailing ! syntax is the recommended convention [4][5][3]. Key details regarding the important modifier in v4: - Consistency with native CSS: Placing the ! suffix at the end of the class makes it easier to reason about, especially when using variants [1][2]. - Usage: Use utility-name! to mark a specific utility as !important [1][4]. - @apply directives: The !important keyword is no longer supported within @apply directives [1][4]. Instead, you should append the ! suffix to each individual utility class used inside the @apply block (e.g., @apply flex! items-center!) [1]. - Global important: If you need to make all utilities !important globally, you can add an important flag to your Tailwind CSS import in your CSS file (e.g., @import "tailwindcss" important;) [6]. Note that in v4, this global flag can also cause utilities used within @apply to automatically receive !important [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Tailwind config files
fd -t f -e js -e ts -e cjs -e mjs -e json -e yml -e yaml tailwind.config . 2>/dev/null || true
fd -t f 'tailwind.config.*' . || true

# Print tailwind config if found (small)
for f in $(fd -t f -a 'tailwind.config.*' .); do
  echo "---- $f ----"
  # limit to first ~120 lines
  sed -n '1,140p' "$f"
done

# Scan for important modifier usage patterns across frontend/src
rg -n --hidden -S "'!px-|\"!px-|!pt-|px-0!|pt-0!|!important" frontend/src || true

Repository: UnlockedLabs/UnlockEdv2

Length of output: 2464


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find tailwind config and print if any (likely small)
fd -t f 'tailwind.config.*' . || true
for f in $(fd -t f 'tailwind.config.*' . 2>/dev/null); do
  echo "---- tailwind config: $f ----"
  sed -n '1,220p' "$f"
done

# Inspect cn implementation (commonly in utils/cn.ts or similar)
fd -t f -e ts -e tsx -e js -e jsx -e mjs -e cjs 'cn.*' frontend/src || true
fd -t f -e ts -e tsx -e js -e jsx -e mjs -e cjs 'utils.*' frontend/src/utils || true

# Directly inspect the component file for imports
sed -n '1,140p' frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx

# Also locate cn import path
rg -n "from ['\"].*cn['\"]" frontend/src/pages/student/digital-transcript/TranscriptResumePreview.tsx || true

Repository: UnlockedLabs/UnlockEdv2

Length of output: 4057


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find tailwind config files (any location)
find . -type f -name 'tailwind.config.*' 2>/dev/null || true
find . -type f -name 'postcss.config.*' 2>/dev/null || true
find . -type f -name 'tailwind.css' 2>/dev/null || true

# Locate cn implementation
fd -t f -e ts -e tsx -e js -e jsx 'utils.*' frontend/src/lib || true
fd -t f -e ts -e tsx -e js -e jsx 'utils.*' frontend/src || true

# Print likely cn file(s)
for f in $(rg -l --hidden "export const cn|export function cn|function cn" frontend/src/lib/utils.ts frontend/src/lib/utils.tsx frontend/src/lib/utils.* 2>/dev/null || true); do
  echo "---- $f ----"
  sed -n '1,220p' "$f"
done

# If not found via rg above, search broadly but limit to first few results
rg -n --hidden "from 'tailwind-merge'|tailwind-merge|twMerge" frontend/src/lib/utils.* frontend/src/lib -S || true
rg -n --hidden "export (const|function) cn" frontend/src -S || true

# Also quickly inspect the imported utils file referenced in TranscriptResumePreview
# (We know it's '`@/lib/utils`'; try to find that path)
fd -t f -e ts -e tsx -e js -e jsx 'lib/utils.*' frontend/src -a || true

Repository: UnlockedLabs/UnlockEdv2

Length of output: 1056


🌐 Web query:

Tailwind CSS v4 important modifier backwards compatibility: does '!px-0' still work or is it only suffix 'px-0!'?

💡 Result:

In Tailwind CSS v4, the recommended syntax for the important modifier is to place the '!' suffix at the end of the utility class (e.g., 'px-0!') [1][2][3][4]. Regarding backwards compatibility, the legacy syntax using the '!' prefix (e.g., '!px-0') is still supported in v4 [5][6]. However, this legacy usage is considered deprecated [4], and you should migrate to the trailing '!' syntax for all new code [7][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Scan for legacy prefix and recommended suffix forms across the whole repo
rg -n --hidden -S "'!px-|!pt-|!m[trblxy]?-|!p[adx]?-|!space|!ring|!text|!border|px-0!|pt-0!|p[adx]?-[0-9-]+!" . || true

# Narrower targeted scans
rg -n --hidden -S "!px-[0-9]|!pt-[0-9]|px-[0-9]!|pt-[0-9]!" frontend/src || true

# Check exact tailwind-merge version from yarn.lock
rg -n --hidden -S "tailwind-merge@" frontend/yarn.lock || true
rg -n --hidden -S "tailwind-merge@\\^3" frontend/yarn.lock || true

Repository: UnlockedLabs/UnlockEdv2

Length of output: 4275


🌐 Web query:

tailwind-merge v3 important modifier syntax '!prefix' vs 'suffix' Tailwind CSS v4

💡 Result:

In tailwind-merge v3, which was released to support Tailwind CSS v4, the important modifier syntax transitioned from a prefix (!prefix) to a suffix (suffix!) [1][2][3]. Key details regarding this change include: * Preferred Syntax: Tailwind CSS v4 utilizes the suffix syntax (e.g., underline!) as the standard way to mark a utility as important [3]. * Backwards Compatibility: While the suffix position is preferred, tailwind-merge v3 maintains support for the legacy prefix syntax (e.g., !underline) to ensure backward compatibility [3]. * Usage in tailwind-merge v3: You can use the suffix syntax directly in your function calls, and tailwind-merge will correctly handle the conflicts [4][5]. For example: twMerge('p-3! p-4! p-5') // → 'p-4! p-5' twMerge('right-2! -inset-x-1!') // → '-inset-x-1!' [4][5] This change aligns tailwind-merge with the official Tailwind CSS v4 syntax, which moved the important modifier to the end of the class name [3]. Upgrading to tailwind-merge v3 is recommended when using Tailwind CSS v4 to ensure full compatibility and proper handling of these syntax updates [1][2].

Citations:


Tailwind v4 important modifier: migrate to suffix syntax for consistency

!px-0 !pt-0 is the deprecated legacy prefix form; Tailwind v4 + tailwind-merge v3 still support it, but the v4-recommended form is suffix (px-0! pt-0!).

Suggested fix
-                    isPaper && '!px-0 !pt-0 pb-4'
+                    isPaper && 'px-0! pt-0! pb-4'
📝 Committable suggestion

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

Suggested change
className={cn(
'space-y-2 border-b border-gray-100 pb-5 dark:border-border',
isPaper && '!px-0 !pt-0 pb-4'
)}
className={cn(
'space-y-2 border-b border-gray-100 pb-5 dark:border-border',
isPaper && 'px-0! pt-0! pb-4'
)}
🤖 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/pages/student/digital-transcript/TranscriptResumePreview.tsx`
around lines 76 - 79, The className string in TranscriptResumePreview's JSX uses
legacy Tailwind important-prefix syntax ('!px-0 !pt-0') when computing classes
via cn; update this to Tailwind v4 suffix important syntax by replacing those
tokens with 'px-0! pt-0!' so the conditional branch that uses isPaper continues
to apply the same important styles using the new suffix form inside the cn call.

: 'text-[#556830] dark:text-primary'
)}
>
Achievements logs of {name}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix user-facing heading grammar.

“Achievements logs of {name}” reads incorrectly; use singular “log” (or “Achievement records”) for polished UI copy.

🤖 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/pages/student/digital-transcript/TranscriptResumePreview.tsx` at
line 91, Update the user-facing heading in the TranscriptResumePreview
component: replace the incorrect phrase "Achievements logs of {name}" with a
grammatically correct variant such as "Achievement record for {name}" or
"Achievement records for {name}" (use the chosen wording wherever the heading
string is rendered and ensure it still interpolates the name variable).

hasIncompleteEntry={hasDraft}
topUserContent={topUserContent}
topFacilityContent={topFacilityContent}
featuredLibraries={featuredLibraries ?? []}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Unnecessary null coalescing for featuredLibraries.

Line 106 uses featuredLibraries ?? [] when passing the prop to ResidentHomeDashboard, but featuredLibraries is destructured from useLoaderData() (line 47) which always returns an object from the loader. The loader at routeLoaders.ts (line 87) always includes featuredLibraries in the returned JSON, even if empty (featuredLibrariesRaw defaults to [] when the API call fails). The ?? operator adds no protection here.

♻️ Minor cleanup
-            featuredLibraries={featuredLibraries ?? []}
+            featuredLibraries={featuredLibraries}
📝 Committable suggestion

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

Suggested change
featuredLibraries={featuredLibraries ?? []}
featuredLibraries={featuredLibraries}
🤖 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/pages/student/ResidentHome.tsx` at line 106, Remove the
redundant null-coalescing when passing featuredLibraries to
ResidentHomeDashboard: since featuredLibraries is obtained from useLoaderData()
and the loader (routeLoaders.ts) always includes featuredLibraries (defaulting
to []), change the prop from featuredLibraries={featuredLibraries ?? []} to
simply featuredLibraries={featuredLibraries}; update the JSX in ResidentHome
(the prop passed to ResidentHomeDashboard) accordingly and run tests/lint to
ensure no further references rely on the fallback.

Comment on lines +349 to +351
const incompleteEntry = useMemo(() => {
return findIncompleteAchievementEntry(learningRecordEntries, learningRecordFormVariant);
}, [hasIncompleteEntry, learningRecordEntries, entrySessionTick, learningRecordFormVariant]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Incorrect dependency in useMemo for incompleteEntry.

The useMemo hook includes hasIncompleteEntry as a dependency (line 351), but hasIncompleteEntry is a prop passed from the parent and isn't used inside the findIncompleteAchievementEntry function. The function only needs learningRecordEntries, entrySessionTick, and learningRecordFormVariant. Including hasIncompleteEntry could cause unnecessary recomputation or, worse, could mask bugs if the parent's hasIncompleteEntry becomes stale relative to the actual learningRecordEntries.

🐛 Fix the dependencies
     const incompleteEntry = useMemo(() => {
         return findIncompleteAchievementEntry(learningRecordEntries, learningRecordFormVariant);
-    }, [hasIncompleteEntry, learningRecordEntries, entrySessionTick, learningRecordFormVariant]);
+    }, [learningRecordEntries, entrySessionTick, learningRecordFormVariant]);
📝 Committable suggestion

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

Suggested change
const incompleteEntry = useMemo(() => {
return findIncompleteAchievementEntry(learningRecordEntries, learningRecordFormVariant);
}, [hasIncompleteEntry, learningRecordEntries, entrySessionTick, learningRecordFormVariant]);
const incompleteEntry = useMemo(() => {
return findIncompleteAchievementEntry(learningRecordEntries, learningRecordFormVariant);
}, [learningRecordEntries, entrySessionTick, learningRecordFormVariant]);
🤖 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/pages/student/ResidentHomeDashboard.tsx` around lines 349 - 351,
The useMemo for incompleteEntry currently includes an unnecessary dependency
hasIncompleteEntry; update the dependency array for the useMemo that defines
incompleteEntry to only include the actual inputs used by
findIncompleteAchievementEntry — learningRecordEntries, entrySessionTick, and
learningRecordFormVariant — so remove hasIncompleteEntry from the dependencies
of the useMemo (the useMemo call creating incompleteEntry that invokes
findIncompleteAchievementEntry).

Comment on lines +12 to +35
{
path: 'learning-record-funnel',
element: <DigitalTranscriptHome />,
errorElement: <Error />,
handle: { title: 'Learning Record' }
},
{
path: 'learning-record-funnel/entry',
element: <DigitalTranscriptEntryPage />,
errorElement: <Error />,
handle: { title: 'Add your achievement' }
},
{
path: 'learning-record-categories',
element: <DigitalTranscriptHome />,
errorElement: <Error />,
handle: { title: 'Learning Record' }
},
{
path: 'learning-record-categories/entry',
element: <DigitalTranscriptEntryPage />,
errorElement: <Error />,
handle: { title: 'Add your achievement' }
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Two route groups render identical components.

Both learning-record-funnel (lines 12-23) and learning-record-categories (lines 24-35) route groups render the same components (DigitalTranscriptHome and DigitalTranscriptEntryPage). If these are intended for variant testing or future differentiation, consider adding a comment explaining the rationale. If they're truly identical, consider consolidating to a single route group.

🤖 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/routes/learning-record-routes.tsx` around lines 12 - 35, The
routes for 'learning-record-funnel' and 'learning-record-categories' both render
the same components (DigitalTranscriptHome and DigitalTranscriptEntryPage);
either consolidate them into a single route group or add an explanatory comment;
specifically, in the route definitions where path: 'learning-record-funnel' and
path: 'learning-record-categories' reference DigitalTranscriptHome and
DigitalTranscriptEntryPage, decide whether to merge into one path (removing the
duplicate group) or keep both and add a clear comment above each route
explaining why both exist (e.g., A/B/variant testing or future divergence) so
future readers understand the intent.

@carddev81

Copy link
Copy Markdown
Contributor

@gtonye pointing out an inconsistency with the grey background. It doesn't span the full viewport like all other pages. See pic below. Click on any other link within the nav bar.

image

@carddev81 carddev81 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tested good and found a few minor issues.

@@ -0,0 +1,39 @@
-- +goose Up
CREATE TABLE learning_record_entries (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gtonye noting that the table should be qualified with the schema name

public.learning_record_entries

also where appropriate please make the change.

@@ -0,0 +1,39 @@
-- +goose Up
CREATE TABLE learning_record_entries (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gtonye this table has created_at and updated_at, but it's missing the rest of our standard auditing set:

  • deleted_at TIMESTAMPTZ
  • create_user_id INTEGER — tracks who created the record
  • update_user_id INTEGER — tracks who last modified it

Can we add these to stay consistent with the rest of our tables (e.g. program_classes)?

Also please make sure that when saving or updating records the values are being set/saved correctly

@@ -0,0 +1,39 @@
-- +goose Up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Was there a specific reason that the version number of the scripts are starting at 00072? The next version number should be 00069?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we can, please use the next available number for consistency

@@ -0,0 +1,10 @@
-- +goose Up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gtonye Not sure why we need this extra migration file, we usually try to use just one migration file per PR. Sometimes we run into a transaction issue where 2 is required. For consistency within our codebase can this bit of code be put into 00072 file.

Comment thread config/dev.nginx.conf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please revert this file back to its original form, this causes local testing to fail

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
backend/src/models/learning_record.go (1)

19-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on unexpected StringSlice.Scan value types.

Line 26-Line 27 silently coerces unknown DB value types to "[]", which can hide schema/data mismatches and lose signal. Return an explicit error for unknown types (and handle nil explicitly if desired).

Suggested fix
+import "fmt"
+
 func (s *StringSlice) Scan(value any) error {
 	var raw string
 	switch v := value.(type) {
 	case string:
 		raw = v
 	case []byte:
 		raw = string(v)
+	case nil:
+		raw = "[]"
 	default:
-		raw = "[]"
+		return fmt.Errorf("unexpected type for StringSlice: %T", value)
 	}
 	return json.Unmarshal([]byte(raw), s)
 }
🤖 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 `@backend/src/models/learning_record.go` around lines 19 - 29, The
StringSlice.Scan method currently silently coerces unknown database value types
to an empty JSON array string in the default case, which hides schema mismatches
and loses error information. Replace the default case that sets raw = "[]" with
an explicit error return statement that indicates an unsupported type was
encountered. If nil values should be handled separately, add an explicit case
for nil before the default case to handle it appropriately, then return an error
in the default case for truly unexpected types.
backend/src/database/learning_record.go (2)

29-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Updates(entry) skips zero-value field writes.

Line 31 uses struct-based Updates, so empty-string/zero/false clears can be dropped during legitimate edits.

Suggested fix
 result := db.Model(&models.LearningRecordEntry{}).
 	Where("id = ? AND user_id = ?", entry.ID, entry.UserID).
-	Updates(entry)
+	Select("*").
+	Omit("id", "user_id", "created_at").
+	Updates(entry)
🤖 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 `@backend/src/database/learning_record.go` around lines 29 - 31, The
`Updates(entry)` call on line 31 uses struct-based updates which causes GORM to
skip zero-value fields (empty strings, false, 0, etc.), preventing legitimate
field clears. Replace the struct-based `Updates(entry)` with a map-based
approach using `Updates(map[string]interface{}{...})` to explicitly specify
which fields to update with their current values from the entry object, ensuring
zero-values can be written. Alternatively, use the `Select()` method before
`Updates(entry)` to explicitly specify which fields should be updated.

29-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat no-match mutations as not found instead of success.

Line 32-Line 35, Line 46-Line 58, and Line 109-Line 112 only gate on Error. RowsAffected == 0 currently returns success, and the delete flow can still write user_account_history for a record that was never deleted.

Also applies to: 46-58, 108-112

🤖 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 `@backend/src/database/learning_record.go` around lines 29 - 35, The code
currently only checks if result.Error is not nil after mutation operations, but
does not verify if any rows were actually affected. At all three affected
locations in the learning_record.go file (lines 29-35 for the Updates call,
lines 46-58 for another mutation operation, and lines 108-112 for the delete
operation), add an additional check for result.RowsAffected == 0 after the error
check. When RowsAffected is 0, treat it as a not found error by returning the
appropriate not found error (similar to newUpdateDBError or appropriate not
found error type) instead of returning nil success, which will prevent
operations like writing to user_account_history when the original record was not
actually modified or deleted.
backend/src/handlers/learning_record_handler.go (2)

33-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force published state on /entries create/update handlers.

Line 33-Line 39 and Line 46-Line 57 still allow client-controlled is_draft on published-entry endpoints. Override entry.IsDraft = false before persistence in both handlers.

Also applies to: 46-57

🤖 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 `@backend/src/handlers/learning_record_handler.go` around lines 33 - 39, The
handleCreateLearningRecordEntry and the update handler (at lines 46-57) allow
the client to control the is_draft field on published-entry endpoints, which is
a security issue. In handleCreateLearningRecordEntry, add entry.IsDraft = false
immediately after decoding the JSON request body and before calling
srv.WithUserContext(r).CreateLearningRecordEntry(&entry). Apply the same fix to
the update handler by setting entry.IsDraft = false before persisting the entry
in that handler as well. This ensures the published-entry endpoints always
enforce published state regardless of client input.

47-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-positive IDs before uint conversion.

Line 47-Line 56 and Line 66-Line 71 can cast negative path IDs into large uint values. Add id <= 0 validation before conversion in both handlers.

Also applies to: 66-71

🤖 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 `@backend/src/handlers/learning_record_handler.go` around lines 47 - 56, After
parsing the ID from the path using strconv.Atoi and checking for parsing errors,
add validation to ensure the ID is positive (greater than 0) before converting
it to uint. A negative integer cast to uint will wrap around to a large unsigned
value, bypassing intended validations. Add an id <= 0 check immediately after
the existing error check and before the uint conversion in both the learning
record handler locations mentioned (around line 47-56 and line 66-71), returning
an appropriate error response if the ID is not positive.
🤖 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 `@backend/src/database/learning_record.go`:
- Around line 99-102: The ON CONFLICT clause in the learning_record.go file (in
the db.Clauses call with OnConflict containing the user_id and client_id
columns) is missing the TargetWhere predicate that matches the partial unique
index definition which includes WHERE deleted_at IS NULL. Add the TargetWhere
field to the clause.OnConflict struct with the condition clause.Where{Exprs:
[]clause.Expression{clause.Expr{SQL: "deleted_at IS NULL"}}} to ensure
PostgreSQL correctly recognizes and uses the partial index, preventing
constraint violations with soft-deleted records. Use the pattern from
program_classes.go as a reference for the correct syntax.

---

Duplicate comments:
In `@backend/src/database/learning_record.go`:
- Around line 29-31: The `Updates(entry)` call on line 31 uses struct-based
updates which causes GORM to skip zero-value fields (empty strings, false, 0,
etc.), preventing legitimate field clears. Replace the struct-based
`Updates(entry)` with a map-based approach using
`Updates(map[string]interface{}{...})` to explicitly specify which fields to
update with their current values from the entry object, ensuring zero-values can
be written. Alternatively, use the `Select()` method before `Updates(entry)` to
explicitly specify which fields should be updated.
- Around line 29-35: The code currently only checks if result.Error is not nil
after mutation operations, but does not verify if any rows were actually
affected. At all three affected locations in the learning_record.go file (lines
29-35 for the Updates call, lines 46-58 for another mutation operation, and
lines 108-112 for the delete operation), add an additional check for
result.RowsAffected == 0 after the error check. When RowsAffected is 0, treat it
as a not found error by returning the appropriate not found error (similar to
newUpdateDBError or appropriate not found error type) instead of returning nil
success, which will prevent operations like writing to user_account_history when
the original record was not actually modified or deleted.

In `@backend/src/handlers/learning_record_handler.go`:
- Around line 33-39: The handleCreateLearningRecordEntry and the update handler
(at lines 46-57) allow the client to control the is_draft field on
published-entry endpoints, which is a security issue. In
handleCreateLearningRecordEntry, add entry.IsDraft = false immediately after
decoding the JSON request body and before calling
srv.WithUserContext(r).CreateLearningRecordEntry(&entry). Apply the same fix to
the update handler by setting entry.IsDraft = false before persisting the entry
in that handler as well. This ensures the published-entry endpoints always
enforce published state regardless of client input.
- Around line 47-56: After parsing the ID from the path using strconv.Atoi and
checking for parsing errors, add validation to ensure the ID is positive
(greater than 0) before converting it to uint. A negative integer cast to uint
will wrap around to a large unsigned value, bypassing intended validations. Add
an id <= 0 check immediately after the existing error check and before the uint
conversion in both the learning record handler locations mentioned (around line
47-56 and line 66-71), returning an appropriate error response if the ID is not
positive.

In `@backend/src/models/learning_record.go`:
- Around line 19-29: The StringSlice.Scan method currently silently coerces
unknown database value types to an empty JSON array string in the default case,
which hides schema mismatches and loses error information. Replace the default
case that sets raw = "[]" with an explicit error return statement that indicates
an unsupported type was encountered. If nil values should be handled separately,
add an explicit case for nil before the default case to handle it appropriately,
then return an error in the default case for truly unexpected types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 57ad14fc-b339-4216-8e36-ccabd8bb9466

📥 Commits

Reviewing files that changed from the base of the PR and between 552410b and 6d20d11.

📒 Files selected for processing (6)
  • backend/migrations/00069_create_learning_record_tables.sql
  • backend/src/database/learning_record.go
  • backend/src/handlers/learning_record_handler.go
  • backend/src/models/learning_record.go
  • backend/src/models/users.go
  • frontend/src/layouts/AuthenticatedLayout.tsx

Comment on lines +99 to +102
if err := db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "client_id"}},
DoUpdates: clause.Assignments(updateMap),
}).Create(draft).Error; err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify partial unique index definition:"
rg -n "CREATE UNIQUE INDEX IF NOT EXISTS idx_learning_record_entries_client_id|deleted_at IS NULL" backend/migrations/00069_create_learning_record_tables.sql

echo
echo "Verify current conflict target in upsert:"
rg -n "OnConflict|Columns:|DoUpdates" backend/src/database/learning_record.go

Repository: UnlockedLabs/UnlockEdv2

Length of output: 478


🏁 Script executed:

# Check GORM documentation and OnConflict usage patterns
rg -n "OnConflict|Assignments" backend/src --type go -A 3 -B 1

echo "---"
echo "Look for other conflict resolution patterns:"
rg -n "clause.OnConflict" backend/src --type go

Repository: UnlockedLabs/UnlockEdv2

Length of output: 3866


🌐 Web query:

GORM OnConflict partial unique index WHERE clause predicate clause support

💡 Result:

GORM supports the WHERE clause predicate required for partial unique index inference in ON CONFLICT clauses via the TargetWhere field in the clause.OnConflict struct [1][2][3]. While the Where field in clause.OnConflict controls the WHERE condition applied to the DO UPDATE action (post-conflict), the TargetWhere field is specifically designed to add the WHERE predicate to the ON CONFLICT (...) portion of the statement, which is necessary for PostgreSQL to correctly infer and utilize partial unique indexes [1][2]. Example usage: go db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "hash_key"}, {Name: "sort_key"}}, // TargetWhere adds the WHERE predicate to the ON CONFLICT target TargetWhere: clause.Where{Exprs: []clause.Expression{ clause.Eq{Column: "tombstone", Value: false}, }}, DoUpdates: clause.AssignmentColumns([]string{"value"}), }).Create(&metadataModel) Key considerations: 1. Distinguishing Predicates: Use TargetWhere for the index inference predicate (e.g., WHERE deleted_at IS NULL) [1][3]. Use the standard Where field only if you need to filter the rows affected by the DO UPDATE action itself [1][4]. 2. PostgreSQL Inference: As required by PostgreSQL, the index_predicate (provided via TargetWhere) must exactly match or satisfy the partial index definition for successful inference [5][6][7]. If inference fails, PostgreSQL will raise an error stating that no unique or exclusion constraint matches the specification [5][7][8]. 3. Schema Definition: Ensure your partial index is correctly defined in your model using the where tag option, such as gorm:"uniqueIndex:idx_name,where:deleted_at IS NULL" [9][10]. Reports indicate that older versions of GORM or specific configurations might experience issues with predicate generation, so it is recommended to use the latest version if you encounter unexpected SQL output [9][11][4].

Citations:


Add TargetWhere to match the partial index predicate.

The partial unique index (migration line 40) includes WHERE deleted_at IS NULL, but the ON CONFLICT clause (lines 100–101) omits this predicate. PostgreSQL won't infer the partial index without a matching predicate, risking constraint violations with soft-deleted records.

Add the TargetWhere field to match the index definition. Reference the pattern already used in program_classes.go line 929:

clause.OnConflict{
  Columns: []clause.Column{{Name: "user_id"}, {Name: "client_id"}},
  TargetWhere: clause.Where{Exprs: []clause.Expression{clause.Expr{SQL: "deleted_at IS NULL"}}},
  DoUpdates: clause.Assignments(updateMap),
}
🤖 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 `@backend/src/database/learning_record.go` around lines 99 - 102, The ON
CONFLICT clause in the learning_record.go file (in the db.Clauses call with
OnConflict containing the user_id and client_id columns) is missing the
TargetWhere predicate that matches the partial unique index definition which
includes WHERE deleted_at IS NULL. Add the TargetWhere field to the
clause.OnConflict struct with the condition clause.Where{Exprs:
[]clause.Expression{clause.Expr{SQL: "deleted_at IS NULL"}}} to ensure
PostgreSQL correctly recognizes and uses the partial index, preventing
constraint violations with soft-deleted records. Use the pattern from
program_classes.go as a reference for the correct syntax.

Comment on lines +20 to +41
const PREVIEW_ITEMS: ContinueLearningItem[] = [
{
id: 'preview-resume-writing',
title: 'Resume Writing Basics',
statusLabel: 'Your progress',
contentType: 'video',
contentTypeLabel: 'Video',
durationLabel: '12 min',
progressPercent: 60,
href: '/knowledge-center'
},
{
id: 'preview-interview-skills',
title: 'Interview Skills',
statusLabel: 'Not started yet',
contentType: 'library',
contentTypeLabel: 'Article',
durationLabel: '8 min',
progressPercent: 0,
href: '/knowledge-center'
}
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there are things hard coded into the homepage - these should be accurate to actual data not just hardcoded into the codebase?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I checked with the team and will remove the ContinueLearningSection as it was part of a concept for the home page but is not related to the digital resident transcript.

@calisio calisio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

things i'd like to see:

Screen.Recording.2026-06-16.at.1.31.15.PM.mov
  • on home screen, dismiss only temporarily dismisses that record

@gtonye

gtonye commented Jun 16, 2026

Copy link
Copy Markdown
Author

things i'd like to see:

Screen.Recording.2026-06-16.at.1.31.15.PM.mov

  • on home screen, dismiss only temporarily dismisses that record

Thank you for the review @calisio

We've made the changes requested. Can you help us better understand this one

on home screen, dismiss only temporarily dismisses that record

Otherwise here is a demo of the recent interaction changes:

pr-1169-drt-ux-fixes.mov

Change Log

  • Connected the sections of the preview view with the questions for the resident.
  • Removed the unused ContinueLearning concept component

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/pages/student/digital-transcript/DigitalTranscriptWysiwygEntry.tsx (1)

776-794: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Delete operation lacks error handling for failed backend calls.

The delete confirmation handler optimistically removes the entry from local state before confirming the backend delete succeeded. If deleteCommittedEntry fails, the UI will show the entry as removed while it still exists on the server.

Consider adding error handling to restore the entry on failure or awaiting the result:

🛡️ Suggested approach
                 onConfirm={() => {
                     const target = deleteConfirmFor;
                     setDeleteConfirmFor(null);
                     if (!target) return;
                     delete baselinesRef.current[target.id];
-                    void deleteCommittedEntry(target.id);
-                    setSession((prev) => {
-                        if (!prev) return null;
-                        const rows = prev.rows.filter((r) => r.id !== target.id);
-                        if (rows.length === 0) return null;
-                        const expandedId =
-                            prev.expandedId === target.id ? null : prev.expandedId;
-                        const lastPreviewId =
-                            prev.lastPreviewId === target.id
-                                ? (rows[rows.length - 1]?.id ?? null)
-                                : prev.lastPreviewId;
-                        return { ...prev, rows, expandedId, lastPreviewId };
-                    });
+                    void (async () => {
+                        try {
+                            await deleteCommittedEntry(target.id);
+                            setSession((prev) => {
+                                if (!prev) return null;
+                                const rows = prev.rows.filter((r) => r.id !== target.id);
+                                if (rows.length === 0) return null;
+                                const expandedId =
+                                    prev.expandedId === target.id ? null : prev.expandedId;
+                                const lastPreviewId =
+                                    prev.lastPreviewId === target.id
+                                        ? (rows[rows.length - 1]?.id ?? null)
+                                        : prev.lastPreviewId;
+                                return { ...prev, rows, expandedId, lastPreviewId };
+                            });
+                        } catch {
+                            // Optionally show error toast or restore baseline
+                        }
+                    })();
                 }}
🤖 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/pages/student/digital-transcript/DigitalTranscriptWysiwygEntry.tsx`
around lines 776 - 794, The delete operation in the onConfirm handler is
optimistically updating local state by calling setSession immediately after
calling deleteCommittedEntry, without waiting for the backend delete to complete
or handling potential failures. If deleteCommittedEntry fails, the UI will show
the entry as deleted while it still exists on the server. Modify the onConfirm
handler to either await the deleteCommittedEntry call and only update local
state if it succeeds, or add error handling that restores the entry in case the
backend delete fails. Ensure the setSession call is conditional on the backend
operation completing successfully.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@frontend/src/pages/student/digital-transcript/DigitalTranscriptWysiwygEntry.tsx`:
- Around line 776-794: The delete operation in the onConfirm handler is
optimistically updating local state by calling setSession immediately after
calling deleteCommittedEntry, without waiting for the backend delete to complete
or handling potential failures. If deleteCommittedEntry fails, the UI will show
the entry as deleted while it still exists on the server. Modify the onConfirm
handler to either await the deleteCommittedEntry call and only update local
state if it succeeds, or add error handling that restores the entry in case the
backend delete fails. Ensure the setSession call is conditional on the backend
operation completing successfully.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b541c972-73f3-41c1-9e63-9c70b2b261f6

📥 Commits

Reviewing files that changed from the base of the PR and between 6d20d11 and 375458b.

📒 Files selected for processing (7)
  • frontend/src/layouts/AuthenticatedLayout.tsx
  • frontend/src/pages/student/ResidentHomeDashboard.tsx
  • frontend/src/pages/student/digital-transcript/AchievementsRecordPreview.tsx
  • frontend/src/pages/student/digital-transcript/DigitalTranscriptWysiwygEntry.tsx
  • frontend/src/pages/student/digital-transcript/LearningRecordDocument.tsx
  • frontend/src/pages/student/digital-transcript/LearningRecordDocumentNarrative.tsx
  • frontend/src/styles/globals.css

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants