URL errors and fuzzy atlas search #60
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gtfs.zone/coloring-book#60
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Two improvements to the Atlas/URL load flow (#36): (1) replace the opaque "Need Help?" toast action on URL load failures with a "More Info" button that opens a modal showing the full error, the attempted URL, and a direct link to open it in a new tab; (2) swap the atlas search's substring filter for uFuzzy so multi-word and out-of-order queries work naturally.
Both changes are self-contained and can be done in either order. The guiding principle is transparency — show the user exactly what failed and give them a direct path to recover.
Relevant Context
Error handling:
loadGTFSFromURL(url)insrc/modules/ui.ts:363— thecatchblock at line 394–418 currently callsnotifications.showError(errorMessage, { actions: [{ id: 'help', label: 'Need Help?', handler: ... }] }). The handler tries to click a help tab; this is the dead code we're replacing.showModalinsrc/modules/modal-utils.ts— takes{ title, body (HTML string), actions, onMount? }. Body is injected as innerHTML so we can include links. This is the right primitive for the error detail modal.urlvariable is in scope in thecatchblock, so it's easy to close over.Fuzzy search:
filterAndRender(query, feeds, resultsEl, onSelect)insrc/modules/atlas-search.ts:36— the filter logic is lines 43–51, a simpleincludes()check. This is the only thing changing.npm install ufuzzy. The API is:new uFuzzy()→uf.search(haystack, needle)returns[idxs, info, order].idxsis the ranked list of matched indices into the haystack array. We build a parallel string array (one string per feed) for uFuzzy to search, then map results back to feeds.Phase 1: URL load error modal
Replace the useless "Need Help?" action with a "More Info" button that opens a transparent error modal.
Steps:
src/modules/ui.ts, in theloadGTFSFromURLcatch block (lines 394–418), change thenotifications.showErrorcall:autoHide: false(errors should persist until dismissed — add this if not already set)actionsarray with a single action:{ id: 'more-info', label: 'More Info', handler: () => showURLErrorModal(url, error as Error) }showURLErrorModal(url: string, error: Error)inui.ts(or a small helper at the top of the file — keep it local, no new file needed):showModalwith:title:'Failed to load feed'body: HTML string containing:<p>with the fullerror.messagein a<code>block (monospace, wrapped) so nothing is hidden<p>with the attempted URL as a clickable<a href="${url}" target="_blank" rel="noopener">link<p class="text-sm text-base-content/60">with the note:"Some feeds block direct browser requests (CORS). You can try opening the link above to download the file, then upload it directly using Load → Upload."actions:[{ label: 'Close', onClick: () => {} }]escapeHtmlthe error message before injecting into innerHTML (the URL goes inhrefso it needs no escaping, buterror.messageis untrusted text)escapeHtmlhelper inui.ts(or reuse if one already exists — grep first)showModalfrommodal-utils.tsinui.tsif not already importedSearch AtlastoFrom TransitLand AtlasGotchas:
urlvariable is captured in thecatchblock closure — confirm it's in scope before thetry(it's a parameter ofloadGTFSFromURL, so it is).showURLErrorModalshould be a plain function, notasync, sinceshowModalreturns a Promise we don't need to await here (fire-and-forget from the notification handler is fine).console.erroron line 395 — keep that for debuggability.Phase 2: Fuzzy atlas search with uFuzzy
Replace the
includes()filter infilterAndRenderwith uFuzzy for ranked, multi-token, order-independent matching.Steps:
npm install ufuzzyandnpm install --save-dev @types/ufuzzy(check if types are bundled — uFuzzy ships its own.d.ts, so@types/may not exist; if not, skip the types install)src/modules/atlas-search.ts, import uFuzzy:import uFuzzy from '@leeoniya/ufuzzy'(check actual package name after install — it may beufuzzyor@leeoniya/ufuzzy)haystack: string[]where each entry is the concatenated searchable fields for that feed:`${feed.name} ${feed.operator_name} ${feed.location} ${feed.url}`. Store it alongsidecachedFeeds.const uf = new uFuzzy({ intraIns: 1 })—intraIns: 1allows one insertion per term, giving basic typo tolerance without getting too loosefilterAndRender, replace the filter logic:qis empty:filtered = feeds(unchanged)qis non-empty: calluf.search(haystack, q)→ destructure[idxs]. Ifidxsis null/empty,filtered = []. Otherwisefiltered = idxs.map(i => feeds[i])(already in ranked order)haystackintofilterAndRenderas a new parameter, or close over it fromshowAtlasSearchModal— the latter is simpler since haystack is built once in the.then()callbackNotes: Package is
@leeoniya/ufuzzy(notufuzzy). Types are bundled indist/uFuzzy.d.ts— no@types/package needed. Haystack stored in module-levelcachedHaystackalongsidecachedFeeds, built inloadAtlasFeeds(). Closed over in.then()callback viaconst haystack = cachedHaystack!.Gotchas:
search()can returnnullforidxswhen there are zero matches — guard withif (!idxs || idxs.length === 0).feedsarray indices — build them together and never sort/mutate either array after construction..toLowerCase()the query anymore.console.logthe import to confirm it loaded.