Skip to content

feat: wanted→offer matching, shown after posting#953

Open
edwh wants to merge 5 commits into
masterfrom
feature/wanted-offer-match
Open

feat: wanted→offer matching, shown after posting#953
edwh wants to merge 5 commits into
masterfrom
feature/wanted-offer-match

Conversation

@edwh

@edwh edwh commented Jul 4, 2026

Copy link
Copy Markdown
Member

Update (review feedback): matches now show after posting, not during compose.
Per @edwh — "get the posts out of them first, then show these after they've posted." The WantedMatches panel has been removed from the find whereami compose step (desktop + mobile) so it never distracts from completing the post. It now renders on the myposts landing page the poster is sent to immediately after posting, for a just-posted WANTED only (query + location derived from the posted item via history state). Dismiss copy updated for the post-success context; added myposts tests (WANTED shows / OFFER hides). The panel reuses the same recommendation-card visual shown in #951. (A live post-success screenshot needs an authenticated session — not capturable from the empty local worktree.)

Summary

Merge after #951 (this branch builds on the similar-posts branch — it shares the reach helper, the MessageSummary-based card approach, and the wanted_match source already wired into the recommendations funnel).

Phase 3 of the vector-search rollout: while someone composes a WANTED, show them existing OFFERs near them that match — so they can grab something that's already available instead of waiting.

  • GET /message/matches — given the item text of a wanted-in-progress and the poster's chosen location, returns existing OFFERs within ~15km that semantically match. Because the wanted isn't saved yet, this embeds the query text via the sidecar (unlike /similar, which reuses a stored embedding), then does the in-memory cosine scan filtered to Offers in a lat/lng box around the location. It applies a search-grade MinVectorScore (this is a match claim, not exploration), reach-filters against the poster's chosen location (a matched offer they couldn't reply to is never shown — reusing Phase 2's shared ReachBlockedSet), and excludes the poster's own offers. Behind a FEATURE_WANTED_MATCH=off killswitch.
  • The panel never blocks posting. WantedMatches.vue renders under the postcode chooser on both the desktop (find/whereami.vue) and mobile (find/mobile/whereami.vue) compose steps once a location is chosen. It's async, emits nothing that gates the Next button, and opens each match in a new tab (window.open(..., '_blank')) so the in-progress draft is never lost. A dismiss link ("Not what I'm looking for - keep posting") collapses it. Even one match is worth showing, so it appears with ≥1 result.
  • Deflection is a success, not a failure. If a composer finds what they wanted among the offers and replies instead of finishing their post, that's the ideal outcome. It's measured through the same recommendations funnel as Phase 2 (source wanted_match): impressions when the panel shows, clicks when a card is opened, and attributed replies within 7 days — all already visible on the ModTools "Recommendations" sysadmin tab.

Failure-safety

The compose flow must never break because matching couldn't run. Every failure path (feature off, no location, empty query, sidecar/embedding unavailable, no matches) returns an empty list and the panel simply hides — there is no path that errors or blocks the poster.

Code Quality Review

(To be completed by the code-review pass before merge.) Reuses Phase 2's shared ReachBlockedSet and the MessageSummary card; the new-tab open via @expand avoids nested-anchor issues while preserving the draft; the route is registered before /message/:ids so "matches" isn't parsed as a message id.

Future Improvements

  • MyPosts "while you wait" placement — the plan also envisaged showing matches on the post-submit MyPosts landing. That needs router-state plumbing (detecting the just-submitted wanted + the user's location) and is deferred; the two in-compose placements deliver the core value.
  • Deflection metric — explicitly tracking "wanted draft abandoned after a match click that led to a reply" would quantify deflection directly, rather than inferring it from the funnel.
  • Offer→wanted direction — the symmetric case (someone posting an Offer sees matching Wanteds) is a natural follow-up.

Test Plan

  • Go (worktree status API): all tests pass (3409✓). New TestMatches* (5): returns nearby same-type offers (excludes Wanteds and out-of-box offers), flag-off empty, no-location empty, excludes the caller's own offers, reach-filtered (out-of-reach dropped, no-reach-row kept).
  • Vitest (worktree status API): WantedMatches.spec.js 6✓ (no render on 0 matches, renders + calls matches with query+location, impression counted once as wanted_match, opens in a new tab tagged wanted_match, dismiss hides, no fetch without a location). Full suite green.

Rollback: FEATURE_WANTED_MATCH=off disables the panel server-side (no deploy).

@netlify

netlify Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploy Preview for golden-caramel-d2c3a7 ready!

Name Link
🔨 Latest commit 62ec367
🔍 Latest deploy log https://app.netlify.com/projects/golden-caramel-d2c3a7/deploys/6a53cefbfbb2d30009bef3e0
😎 Deploy Preview https://deploy-preview-953--golden-caramel-d2c3a7.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@FreegleGeeks FreegleGeeks added CI: Running CI build is currently running CI: Failed CI build failed and removed CI: Running CI build is currently running labels Jul 4, 2026
@edwh edwh force-pushed the feature/wanted-offer-match branch from e930c53 to 50b6a4b Compare July 4, 2026 21:05
@FreegleGeeks FreegleGeeks added CI: Running CI build is currently running CI: Passed CI build passed and removed CI: Failed CI build failed CI: Running CI build is currently running labels Jul 4, 2026
@edwh edwh changed the title feat: wanted→offer matching at post time feat: wanted→offer matching, shown after posting Jul 5, 2026
@FreegleGeeks FreegleGeeks added CI: Running CI build is currently running CI: Passed CI build passed and removed CI: Passed CI build passed CI: Running CI build is currently running labels Jul 5, 2026
@edwh edwh force-pushed the feature/wanted-offer-match branch from dfd2b96 to 387e215 Compare July 5, 2026 17:36
@FreegleGeeks FreegleGeeks added CI: Running CI build is currently running CI: Passed CI build passed and removed CI: Passed CI build passed CI: Running CI build is currently running labels Jul 5, 2026
@edwh edwh added the Blocked PR is blocked and cannot be merged label Jul 6, 2026
edwh added 5 commits July 12, 2026 18:28
…urement

- GET /message/:id/similar: stored-embedding cosine, same-type, excludes
  source+own-author, MinSimilarScore, reach-filtered (shared ReachBlockedSet
  helper, also used by the browse read path), FEATURE_SIMILAR_POSTS killswitch.
- Source-tagged impressions via MarkSeen (first-touch), threaded through the FE.
- SimilarPosts.vue: lazy strip on the message page reusing MessageSummary,
  10% userid holdout, >=3-results gate, one source-tagged impression per view.
- Recommendations sysadmin tab: per-source impressions/clicks/CTR/attributed
  replies (7-day window) + holdout-vs-shown reply-rate comparison.
…shing message page)

The v-observe-visibility binding used {callback, once, intersection} but the
plugin expects {callback, options}. With no 'options' key it read
options.observeFullElement off undefined at beforeMount, throwing
'Cannot read properties of undefined (reading observeFullElement)' and turning
every message page into a 500 error page (caught by Playwright, not vitest which
stubs the directive). Match OurMessage.vue's proven shape; this plugin has no
'once' so guard re-entry with a loaded flag; add .catch() to the fire-and-forget
markSeen (review finding).
- GET /message/matches: embeds the wanted's item text, finds Offers within
  ~15km of the poster's chosen location, reach-filtered against that location
  (shared ReachBlockedSet), excludes own posts, FEATURE_WANTED_MATCH killswitch.
- WantedMatches.vue: async, non-blocking panel under the postcode chooser on the
  desktop and mobile compose steps; reuses MessageSummary; opens each match in a
  new tab so the draft is preserved; dismissable. Impressions tagged wanted_match
  (already in the recommendations funnel).
- Every failure path (off / no location / no match / sidecar down) hides the
  panel; it can never break or block posting.
Addresses the Phase 3 review: a monotonic load token discards an out-of-order
response from a superseded query/location, and the fire-and-forget markSeen now
.catch()es so a non-401 rejection never surfaces as an unhandled promise.
Per review: don't distract the poster mid-compose. Remove the WantedMatches
panel from the find whereami step (desktop + mobile) and instead show it on the
myposts landing page the poster is sent to right after posting - but only for a
just-posted WANTED (derived from the posted item + location in history state).
Updates the dismiss copy for the post-success context and adds myposts tests
covering the WANTED-shows / OFFER-hides cases.
@edwh edwh force-pushed the feature/wanted-offer-match branch from 387e215 to 62ec367 Compare July 12, 2026 17:29
@FreegleGeeks FreegleGeeks added CI: Running CI build is currently running CI: Passed CI build passed and removed CI: Passed CI build passed CI: Running CI build is currently running labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Blocked PR is blocked and cannot be merged CI: Passed CI build passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants