Skip to content

Optionally show booked times as greyed out on the booking page #19

Closed

Description

@shockalotti

Requested in #14, agreed there after discussion.

Optionally show times that are already booked as greyed-out, non-clickable entries in the slot list, instead of omitting them. For public-hours use cases (intro calls, clinics, tutoring) a visibly busy calendar communicates demand and limited availability; today an empty-looking list reads as "nothing here".

Per event type, default off. Reasoning is in the discussion: the endpoint is public and unauthenticated, and while the host's schedule is already inferable by comparing days or by polling and diffing, greying makes that effortless and exact for a casual visitor. Fine when hours are public, not fine for an instance fronting internal team calendars.

Scope it narrowly

Grey only slots blocked by a booking or a calendar conflict, inside the host's offered hours.

Do not render:

  • times outside the host's availability rules (that's the whole rest of the day, and it's the part that leaks the frame)
  • times unavailable only because of min_notice_minutes - greying those claims "someone booked this" when nobody did, corrupting exactly the signal this feature exists to send. Those get the hint in Booking page: explain empty slot lists and minimum-notice gaps #20 instead.

Buffer-blocked slots are a judgement call; leaning toward greying them, since they genuinely are unbookable.

Implementation notes

  • slots.Generate (internal/slots/generate.go) never materialises unavailable starts. It subtracts busy from the working windows and then walks what remains, so the skipped starts don't exist as objects. It has to emit them, tagged with a reason, for this to be possible at all.
  • Do not surface unavailable slots through MCP get_available_slots or the booking assistant. The assistant's invariant is that it only ever sees computed availability; hand it a list containing unbookable entries and it will eventually offer one.
  • Latent bug to avoid: book.html derives which calendar dates are clickable from Object.keys(slotsByDay). Mix unavailable slots into that structure and a fully booked day starts looking bookable. The available-dates set must count bookable slots only.
  • Watch payload size on the month prefetch.
  • Three surfaces: book.html, manage.html, embed.js. Markup and JS are not shared; styling is (booking.css). Verify desktop and mobile on each.

Activity

  1. pullfrog commented on Sep 1, 2026

    @pullfrog
    Contributor
  2. shockalotti commented on Sep 2, 2026

    @shockalotti
    ContributorAuthor

    Engine and API are on dev; the three booking surfaces are still to do.

    Engine (b172eac). GenerateWithTaken sits alongside Generate, sharing the same walk. "Taken" is defined by difference rather than by inspecting reasons: a start is taken if it would have been offered with no busy intervals at all, and is not offered with the real ones.

    That definition turned out to be doing real work. It is correct for every routing mode without special-casing any of them, and it makes three mislabellings structurally impossible rather than merely untested - a start outside the host's working hours, one withheld by minimum notice, and one lost to a host pool that cannot satisfy the routing mode are all absent from both passes, so they cancel. Each has a test, because greying a slot asserts "somebody booked this" and saying that about a time the host does not work would both mislead the booker and expose the shape of the working day.

    Buffers do count as taken, deliberately. Taken slots carry no host ids: the page needs the time, and naming who is busy discloses more about an individual than greying requires.

    API (6d8174e). show_taken_slots per event type, migration 00057, default off. computeSlots gained an explicit includeTaken parameter so all three call sites have to state intent; the MCP tool and the booking assistant pass false and there is a test at the MCP surface, with the event type opted in, asserting no taken field reaches an agent.

    One bug the tests caught first: taken was present even when not opted in, because converting an empty result yields an empty non-nil slice. The result now carries an explicit flag, which also keeps "opted in, nothing booked today" distinct from "does not show taken times".

    Next: book.html, manage.html and embed.js, plus the editor toggle.

  3. shockalotti commented on Sep 3, 2026

    @shockalotti
    ContributorAuthor

    Shipped in 0.8.0, across b172eac (engine), 6d8174e (schema + API), 7370179 (three surfaces + editor) and be500e4 (docs).

    Enabled per event type under Visibility, off by default. GET /v1/event-types/{slug}/slots returns a taken array for opted-in event types and omits the field entirely otherwise, so a client can tell "does not show taken times" from "opted in, nothing booked today".

    The three scoping decisions in the issue body all held:

    • Only starts removed by a booking or calendar conflict are shown. GenerateWithTaken computes that as the difference between a normal pass and one ignoring busy, which makes out-of-hours, min-notice and routing-mode gaps structurally incapable of being labelled as booked rather than merely untested.
    • Min-notice gaps are excluded and get plain-language copy instead, tracked as Booking page: explain empty slot lists and minimum-notice gaps #20.
    • Agents never see taken slots. computeSlots takes an explicit includeTaken so all three call sites state intent, and there is a test at the MCP surface, with the event type opted in, asserting no taken field reaches an agent.

    The available-dates trap the issue called out is handled by bookableDayKeys in the shared booking-logic.js: grouping taken slots by day means a fully booked day still produces a key, so the calendar can't derive clickable dates from those keys. Fully booked days are still openable on purpose - a struck-through list explains itself better than a dead date.

    Verified in a real browser on all three surfaces plus mobile, with the editor toggle round-tripped both ways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions