Skip to content

Agent duplicate-check, session persistence, proposal delete, WebGL noise, address-crash fix#93

Merged
zinkh merged 5 commits into
mainfrom
claude/tenant-logo-header-181tbv
Jul 19, 2026
Merged

Agent duplicate-check, session persistence, proposal delete, WebGL noise, address-crash fix#93
zinkh merged 5 commits into
mainfrom
claude/tenant-logo-header-181tbv

Conversation

@zinkh

@zinkh zinkh commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Agent duplicate-checkcreate_record now 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 returns needs_confirmation with 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 with confirm: true. Also adds a search_records tool so an agent can look up a record's id by keyword instead of guessing.
  • Session persistence fixgetAccessToken() and the initial session check no longer wipe the stored Supabase session on a mere getSession()/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.
  • Proposal delete vs. reject — a Draft proposal was never sent, so "rejecting" it didn't make sense. Adds DELETE /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.
  • WebGL context-loss noise — MapLibre's own fallback for an unhandled internal error event is console.error(...), and this app's Sentry setup captures every console.error as a tracked error. Registered a proper error listener 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 a beforeSend filter as a backstop.
  • Address-crash fix — the app has exactly one error boundary, wrapping all of <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. Added InfoPanelBoundary and 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) passes
  • Verified via Supabase logs that the session/team_members fixes behave as expected in production
  • Manual click-through of proposal delete (Draft) vs. reject (Sent) in the UI
  • Manual test of an unrecognized address in the Proposals form to confirm the panel degrades gracefully instead of crashing

Generated by Claude Code

claude added 5 commits July 19, 2026 19:39
…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.
@zinkh
zinkh merged commit 0a9a53c into main Jul 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants