Agent duplicate-check, session persistence, proposal delete, WebGL noise, address-crash fix#93
Merged
Merged
Conversation
…sources Previously an agent with write access would call create_record straight away, with no check for whether the thing already existed — so asking an agent to add a contact or a project could silently create a duplicate instead of catching that one already exists and asking whether to update it or create a new one anyway. create_record now runs a duplicate check itself before writing: each resource declares an identityField (title/name/...; contacts are special-cased on company_name or first+last name), and if an existing record shares that identity, the call returns needs_confirmation with the matching id(s) instead of creating anything. The system prompt requires the agent to surface those matches and get explicit user confirmation before either updating the existing record or retrying create_record with confirm: true — never silently, and never within the same back-and-forth. Also adds a search_records tool (keyword search over a resource, for resources with an identity field) so an agent can look up an existing record's id before update_record/delete_record when the user doesn't give it directly, instead of guessing.
…ssion getAccessToken() and the initial session check in UserContext both wrapped getSession()/refreshSession() in an 8s timeout and, on ANY failure — including a bare timeout from a slow network or a stuck cross-tab lock — wiped the entire stored Supabase auth token, forcing a fresh login. A timeout just means "no answer in time", not proof the refresh token is actually dead, so this was needlessly logging people out anytime a token refresh (which only kicks in once the ~1h access token has expired) was merely slow rather than genuinely failed. Now only a definitive response from Supabase (refreshSession() actually returning an auth error, meaning the refresh token is confirmed expired/ revoked) clears the stored session. A timeout just fails that one call/ load and falls back to the login screen without touching storage, so a transient blip self-heals on the next reload instead of requiring the user to re-enter their credentials.
…t ones A Draft proposal was never sent to a client, so "rejecting" it doesn't make sense — there was nothing to reject. Only proposals that have actually been sent should be marked Rejected, to keep a record of the outcome; an unsent draft should just be deleted outright. Adds DELETE /api/proposals/:id (didn't exist before), guarded server-side to only allow deletion while status is Draft — a proposal already sent can't be deleted through this endpoint even if called directly, only rejected via the existing PUT. The red cross in Proposals.tsx now branches on status: Draft asks for delete confirmation and removes the proposal; anything else (Sent, etc.) behaves as before and rejects it. Also flips proposals.delete to true in the agent action registry now that the capability genuinely exists — the server-side Draft-only guard applies there too, so an agent can't delete a sent proposal either.
Traced this to MapLibre's own Evented class: any internal error fires an 'error' event, and if nothing is listening, its documented fallback is console.error(event.error) — "to ensure that no error events are dropped". MapLibreCadastre never registered an error listener, so every internal error (WebGL context loss among them, but also things like a failed tile request) fell through to console.error, and this app's Sentry setup captures every console.error as a tracked error. WebGL context loss is a normal, recoverable GPU condition (low memory, backgrounded tab, too many contexts on lower-end devices) that this component already has dedicated recovery UI for (the "Contexte WebGL perdu" overlay + reload button). Registering a proper error listener downgrades it (and other non-fatal MapLibre errors) to console.warn, which Sentry doesn't capture, instead of letting it default all the way up to a tracked error for a condition that's already handled. Also adds a beforeSend filter in the Sentry config to drop this specific message as a backstop, in case it ever reaches Sentry through a path other than the console.error fallback this fix closes.
The app has exactly one error boundary, wrapping all of <App /> in main.tsx. Proposals/Projects/ProjectDetail all render several address- dependent widgets (RNB, BDNB, cadastre, PLU, Géorisques, monuments historiques, the cadastre/OSM/Géorisques maps) as soon as any text is typed in the address field — not just once a suggestion is actually picked. If a typed address can't be resolved by the underlying geocoding/cartography APIs and one of those widgets throws while rendering, there was nothing between it and the top-level boundary: the entire app unmounts to the generic error screen, taking whatever the user had typed into the form (a proposal that was never saved) down with it — matching "l'adresse n'est pas reconnue, la proposition ne peut pas être enregistrée". Adds InfoPanelBoundary, a thin per-widget wrapper around Sentry's ErrorBoundary (works standalone even without a DSN configured — the catch/fallback mechanics don't depend on the client being initialized) with a small inline "informations indisponibles" fallback. Wraps each address-dependent widget individually in all three pages so one resolvable failure only blanks that one panel — the rest of the form, and the save button, stay intact.
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.
Summary
create_recordnow checks for an existing record with the same identity (name/title, or company/first+last for contacts) before writing anything. If a match is found it returnsneeds_confirmationwith the matching id(s) instead of creating a duplicate; the system prompt requires the agent to get explicit user confirmation before either updating the existing record or retrying withconfirm: true. Also adds asearch_recordstool so an agent can look up a record's id by keyword instead of guessing.getAccessToken()and the initial session check no longer wipe the stored Supabase session on a meregetSession()/refreshSession()timeout. Only a definitive "refresh token is dead" response from Supabase clears it now, so a slow network or a stuck cross-tab lock no longer forces an unnecessary re-login.Draftproposal was never sent, so "rejecting" it didn't make sense. AddsDELETE /api/proposals/:id(didn't exist before), guarded server-side to Draft-only. The red cross in the Proposals list now deletes (with confirmation) while a proposal is a draft, and rejects it as before once it's been sent.errorevent isconsole.error(...), and this app's Sentry setup captures everyconsole.erroras a tracked error. Registered a propererrorlistener on the cadastre map so WebGL context loss (a normal, recoverable GPU condition the component already has recovery UI for) logs quietly instead of being reported as a bug. Added abeforeSendfilter as a backstop.<App />. Several address-dependent widgets (RNB, BDNB, cadastre, PLU, Géorisques, monuments historiques, map previews) render as soon as any text is typed into an address field, and if one throws on an address it can't resolve, it used to blank the entire app — taking an unsaved form down with it. AddedInfoPanelBoundaryand wrapped each widget individually in Proposals, Projects, and ProjectDetail, so one panel failing no longer takes out the whole form.Test plan
npm run lint(tsc --noEmit) passesGenerated by Claude Code