Commit Graph
2 Commits
Author SHA1 Message Date
serfaandClaude Opus 5 0c49aa9502 Deck: five actions — rewind, watch and ask, either side of pass and send
The deck had two answers, which is a lot to hang on a swipe: send this pro a job
right now, or lose them. Three more, in one fixed row (DESIGN.md §6.8):

  rewind · ✗ · watch · ✓ · ask

Size is the hierarchy — the two that end the card stay 64px, the three that do
not are 44px, never below the §8 floor. Rewind is disabled rather than hidden
when there is nothing to undo, so the row never changes length and the big pair
never moves out from under a thumb.

Rewind is local. The entry deck writes no swipes — a `swipes` row is job-scoped
and there is no job there — so the card leaving was only ever an index move.

Watch: "tell me when this one is free"
- `pro_watches` snapshots the pro's availability AT WATCH TIME, because the
  trigger is a change, not a state. Without it a sweep would notify every
  watcher on every run, since "available" stays true for as long as they stay
  available.
- Deliberately the narrow version: a pro with is_accepting_jobs = false is
  invisible everywhere (eligibleProAtAnyDistance requires it), so a watch can
  only be placed on somebody already free and fires on the away-and-back cycle.
  "Free at a time that suits me" needs pro_availability — seeded since M1, read
  by nothing — to become a real calendar. Flagged rather than faked.

Ask: a question, before there is a job
- This is the first way to reach a pro who has not agreed to anything. Chat was
  gated behind message → match → accepted request → job, and that gate is what
  made a pro's inbox worth opening, so the cap is not decoration:
  MAX_OPEN_ENQUIRIES unanswered at a time, one thread per pair so it cannot be
  walked around, answered threads stop counting, stale ones fall out, and the
  pro can close one.
- `enquiries` is its own table, not a match with a null job: a match means a pro
  said yes to specific work, and collapsing the two would put rows in `matches`
  that no quote, booking or review could hang off.
- `messages` now belongs to a match OR an enquiry, with a CHECK making the
  illegal state unrepresentable. One message table, so one chat screen.

283 tests passing; typecheck and lint clean across 7 packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 06:49:17 -04:00
serfaandClaude Opus 5 974e312534 M2: the full job lifecycle — chat, hiring, geocoding, quotes, bookings, reviews
Closes the funnel. Before this the product could match two people and then
stopped: `quotes`, `bookings` and `reviews` had tables and state machines and
nothing that wrote a row, the entry deck's right swipe was wired to an empty
handler, and every address resolved to the city centre.

Jobs tab and chat
- message router: thread, send, markRead, unreadTotal. A thread is a MATCH, not
  a job — one job with three interested pros is three private conversations.
- Current/Past segments derived from ACTIVE_JOB_STATUSES, job detail listing the
  pros who accepted, and the conversation itself with attachments.

Hiring from the deck
- A right swipe on the entry deck opened nothing. It now resolves "which job?"
  through a sheet — sign in, pick an open job, or post one — and calls the same
  deck.swipe the per-job deck does, so the open-request cap and row lock apply
  exactly once. Swipes are vetoable so closing the sheet returns the card.

Geocoding
- ST_Distance and ST_DWithin rank and filter every deck, and both operands were
  placeholders. Addresses now resolve through Mapbox (permanent=true, which is
  what licenses storing the coordinates), the server resolves points rather than
  trusting client-supplied lat/lng, and every stored point records how it was
  obtained. A `city`-precision base cannot reach the verification queue.

Quote -> booking -> review
- The commercial chain, minus payments. Accepting a quote is the only place a
  booking is created; confirming completion is what unlocks reviews and moves
  the pro's completed_jobs.
- Reviews publish double-blind with no sweeper: each is written with
  published_at already set to its embargo deadline and every read filters
  published_at <= now(), so it publishes itself. The second review pulls both
  forward. A silent counterparty cannot bury a bad review by never replying.

State machine changes, both deliberate
- booked -> matched: a cancelled booking is not a cancelled job.
- scheduled -> awaiting_confirmation: in_progress is optional, so a pro who
  never tapped Start can still say the work is done.

Test suite
- api tests ran files in parallel against one database and failed roughly one
  run in three on whichever file lost the race. Serialised, and three fixtures
  that grabbed "the first client" pinned to the seeded accounts.

Also includes work from a parallel session: admin verification queue, pro
public profile and reviews read path, notification sending, denormalised stats
recompute, search, and observability.

318 tests passing; typecheck and lint clean across 7 packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 06:29:59 -04:00