Client books that sort themselves into five chapters, with AI working quietly underneath. Static clickable mockups of the target UX, plus the data model and rules to build it on the current Next.js and Supabase stack.
Newly Added, Final Selections, Toured, Reviewed, Not A Fit. Each space is in exactly one, derived from events, never dragged by hand after every tour.
Newly Added means new to the person looking. Computed from added_at vs. that viewer's opens, not a global flag.
Each space keeps the number assigned when added. Reordering never renumbers, so "No. 7" in an email stays correct.
Total, toured, finals. First thing a client sees, and a filter for the list and the map.
No chatbot. A since-you-were-here line, a why-it's-here line, and broker tools that output a checklist of changes.
Every AI-proposed change goes through a broker approve step. 30-day undo via the event log.
Each phone is live: tap cards, buttons and back arrows to move between screens. Scroll sideways for all eight.
not_a_fit event by the broker or any client viewer, not reversed.stage = final. A client Top Choice only suggests it.added_at after this viewer's first open of the book and no space_opened event by this viewer, or featured_until in the future.| Table | Columns |
|---|---|
book_spaces | space_no (int, immutable), stage (enum: none, toured, final, not_a_fit), sort_key (text, fractional index), added_at, featured_until |
book_events | id, book_id, space_id, actor_id, actor_role, type, payload jsonb, created_at. Append-only. Source of truth. |
book_viewers | last_seen_at, first_opened_at |
not_a_fit_reasons | event_id, reason (light, layout, location, price, size, building, commute, other), note, share_with_team |
space_added space_opened reaction not_a_fit stage_changed tour_scheduled tour_completed comment proposal_applied undo
Command bar and voice debrief share one server route. The model returns structured output only; the server validates every space_id against the book before showing it. Rent and lease economics never go into the prompt.
POST /api/books/:bookId/proposals
{ "mode": "command" | "voice", "input": "text or transcript" }
200 {
"changes": [
{ "space_id": "uuid", "from": "none", "to": "final", "reason": "Starred by client, toured Oct 14" }
],
"notes": [ { "space_id": "uuid", "text": "Client concerned about columns", "visibility": "internal" } ],
"flags": [ { "space_id": "uuid", "type": "split", "detail": "1 star, 1 not a fit" } ]
}
POST /api/books/:bookId/proposals/:id/apply { "accepted_ids": [ ... ] }
writes one stage_changed event per accepted change, inside one transaction
sort_key so a drag updates one row.book_spaces and book_events so open books update live.