fix: surface report failures, and consolidate the widget fallback - #64
Merged
Conversation
sessionReport dropped the error from all four of its Supabase queries, so a permissions failure or a bad column returned [] and the UI rendered 'No students have opened this pathway yet.' A real failure was indistinguishable from a class that had not started. Three queries now log and degrade; the interactions query throws, because every number in the report is derived from those rows -- a failure there is a wrong report rather than an empty one. The route catches, logs, and returns 500 with a generic message, keeping Postgres detail out of the response. Both report components already had setError UI that could never fire: .catch() only sees network errors, so a 500 carrying valid JSON flowed straight into setRows. They now check res.ok, which is what actually makes the error visible.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Twelve of the sixteen widget generators each carried the same block: call the fallback generator, then merge its note with a kind-specific message. attemptStepWidget did the same thing again, so the fallback was written in thirteen places.
Generators now return { widget: null, note } saying why they could not produce something usable, and attemptStepWidget does the single fallback call. The kind-specific messages are unchanged; crossword's tail collapses to
eturn normalize(spec), which is the shape it already had.
Fixes a real bug on the way: attemptStepWidget composed [substitutionNote, result.note] and dropped the fallback's own note, so on the four paths where the outer fallback actually fired that text was lost. All three now compose.
No token saving -- the two layers never stacked, because the twelve returned the fallback's non-null widget and the outer branch could not fire for them. This is about one policy instead of thirteen, and a contributor not having to know to write that block.
…hable attemptStepWidget ended in widget as WidgetSpec, which quietly turned a null into a spec. That was survivable while twelve generators had their own fallback; now that swiper-flashcard is the only net it throws instead, into generateStepWidget's existing retry. PathwaysDashboard had the same dead error state as the session report: .catch() only sees network failures, so a 500 with a JSON body flowed into setSessions and sessions.length ran on an object. It checks res.ok now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three related changes in the pathway pipeline: a failure that rendered as an empty class, the same defect in a second component, and a fallback policy written out thirteen times.
1. The session report showed an error as "no students yet"
sessionReportdestructured only{ data }from all four of its Supabase queries. Any failure — a permissions change, a renamed column, RLS — leftdatanull, the function returned[], and the teacher saw:No log, no status code, nothing in the network tab. Given the roster tables are the ones with an outstanding
revoke/RLS question, a permissions failure is both the most likely trigger and the one that looks most like ordinary emptiness.Fixing the storage layer alone would have surfaced nothing. Both consumers already had error UI that could never fire:
So all four layers had to change together:
supabase.tsreport/route.tsSessionReport.tsxres.okSessionReportPage.tsxjson()helper, checksres.okon both fetchesOnly the interactions query throws: every number in the report derives from those rows, so its failure is a wrong report rather than an empty one. The other three can legitimately return nothing, so they follow this file's existing convention for reads. The route keeps Postgres detail server-side, since error messages can carry schema and column names.
PathwaysDashboardhad the identical dead error state and gets the same one-line fix.2. One fallback policy instead of thirteen
Twelve of the sixteen widget generators each carried the same block — call the fallback generator, merge its note with a kind-specific message — and
attemptStepWidgetdid it again.Generators now return
{ widget: null, note }saying why they couldn't produce something usable, andattemptStepWidgetmakes the single fallback call. Kind-specific messages are unchanged.crossword's tail collapses toreturn normalize(spec), which is the shape it already had. All twelve dropfallbackWidgetKindandgetWidgetGeneratorfrom their imports.This fixes a real bug.
attemptStepWidgetcomposed[substitutionNote, result.note]and dropped the fallback's own note, so on the paths where the outer fallback actually fired, that text was lost. All three now compose.It also ended in
widget as WidgetSpec, which quietly turned a null into a spec. That was survivable while twelve generators had their own fallback; now thatswiper-flashcardis the only net, it throws instead — intogenerateStepWidget's existing retry, so a transient model failure gets a second attempt rather than rendering a blank step.swiper-flashcardis now the sole net. Checked: itsnormalizecan't return null and itsgeneratenever returns a null widget, so it is genuinely terminal — but it is a single point of failure by design, which is what the new guard is there to make loud.18 files, +91 / −117.
pnpm lint0 errors,pnpm buildexit 0.Same defect class, deliberately left out
Found while sweeping for the same two patterns, not included to keep this reviewable:
AssignToStudents.tsx:37— a failed assignments fetch makes a student look unassigned, so a teacher can double-assign. Real consequence, but the per-item.catchmay be intentional.RosterPage.tsx:17,AssignToStudents.tsx:27— guarded byArray.isArray, so a 500 silently shows an empty roster. Neither component has error UI, so fixing means designing one.error, notablyloadProfile,fetchMasteryRollupandfetchRecentInteractions. Those three feed the student profile, so a failure yields a silently un-personalised pathway rather than a visible blank — the hardest kind to notice, and the best candidate for a follow-up.learn/page.tsx:55andSharedPathwayView.tsx:29look like the same bug but already checkresponse.ok; the remaining bare.catch(() => {})sites are deliberate fire-and-forget.