Context
The troubleshooting feature lets admins/hosts understand why slots are unavailable on the public booking page. A working but incomplete version exists on main. The prototype branch (troubleshoot-revamp-functional) demonstrates all the desired behaviors but is not production-ready code.
This plan breaks the revamp into 5 phases, each a separate PR. Each phase builds on the previous and is independently testable.
What Exists on Main Today
Basic troubleshoot mode toggle (Start/Done troubleshooting button)
fillFullDaySlots()fills the full day with slots in troubleshoot modeSlot-level HelpPopover showing one
generalUnavailablereason per slotPer-member breakdown for round-robin/multihost (available vs unavailable members)
Backend generates:
lead_time,holiday,host_availability,host_availability_override,booking,booking_before/after_buffer,google/outlook/icloud_event,icloud_event_travel_time,calendar_event_before/after_buffer,slot_before/after_buffer,outside_range,booking_limit_per_slot_reached,overlapping_group_slot_bookingDay-level:
fullyUnavailableflag set forlead_time,holiday, and all-members-unavailableTranslations for slot-level reasons exist
What's Missing (the gaps this revamp fills)
Multiple reasons per slot — backend only stores one
general_unavailableper slot; needsgeneral_unavailablesarray to accumulate all reasonsdate_range_exceededreason — backend doesn't generate this; missing dates have no reasonavailable_due_to_overrideindicator — backend doesn't mark slots that are available only because of an overrideBooking limit day-level reasons — day marked unavailable but no reason exposed (
booking_limit_reached_day/week/monthat day level)Host-level booking limit reasons —
_hostvariants for per-host limits"Whole day unavailable" summary card — doesn't exist; user must click each slot
Past time as day-level reason — when the entire day is in the past, no day-level banner is shown
Per-host day-level breakdown — for round-robin/multihost, no per-host reasons at day level
Change/View action links — no links to admin pages where the issue can be resolved
Timezone warning overlay — doesn't exist
Calendar day grey styling — unavailable days not visually distinct in troubleshoot mode
Day-level reason translations —
day.*i18n keys don't existduration_mismatchreason — not generated by backendmembers_unavailablereason — not generated by backendhost_availability_no_hoursreason — not distinguished fromhost_availabilitytoo_soonreason — not generated by backend or frontendReason display ordering — reasons shown in arbitrary order, spec defines a precedence
Booking limit numbers — reason payloads don't include the actual limit value (
limit_no), only "has been reached"Round-robin host ordering — available/unavailable host sections don't flip order based on slot availability status
Phase 1: Backend — Accumulate All Reasons + New Reason Keys
Goal: Make the backend the single source of truth for all unavailability reasons. After this phase, the API response in troubleshoot mode contains everything the frontend needs.
Scope: Backend only (time_frames_to_slots_service.rb + related services). No frontend changes.
Tasks
Accumulate multiple reasons per slot
Add
general_unavailablesarray alongside existinggeneral_unavailable(keep the old field for backward compat)When a slot is marked unavailable by multiple reasons (e.g., holiday + lead_time + booking conflict), all reasons appear in the array
For per-member reasons: add
unavailable_reasonsarray to each member entry (alongside existingunavailable_reason)
Add date_range_exceeded reason
When a date falls outside the meeting's configured date range (
start_date/end_dateor days-into-future), generate this reasonApplies to missing dates that currently have
fullyUnavailable: truebut no reasonAdd
outside_schedulable_range?helper method
Add available_due_to_override flag
After all slots are generated, check each available slot: if it falls on an override date AND outside the member's original working hours, mark
available_due_to_override: truewithavailability_nameRequires tracking
original_periods_by_wdayandoverride_datesfrom member time frames
Enhance day-level reason collection
Add
collect_day_level_reasonsmethod that checks if all slots share common blockers (booking limits, holiday, date_range_exceeded, past time)Expose
general_unavailablesarray at the date level (not justgeneral_unavailable)For round-robin/multihost: add
extract_day_level_membersthat provides per-host day reasonsAdd
past_timeas a day-level reason when the entire date is in the past
Add booking limit host-level reasons
When a host's personal booking limit is reached, generate
booking_limit_reached_day/_week/_monthwithlimit_scope: :memberandhost_namein the payloadThe frontend maps
limit_scope: "member"to_hosttranslation keys (e.g.,booking_limit_reached_day_host) — the backend does NOT generate_hostsuffixed reason keys directly
Add duration_mismatch detection
When a slot's meeting duration extends beyond the availability window end, mark it with
duration_mismatchreasonThe prototype computes this on the frontend — production implementation must move it to the backend
Add too_soon reason
When a slot falls within the lead time window, generate
too_soonreasonTranslation exists but reason is not yet generated by backend or frontend
Add members_unavailable reason
For round-robin/multihost, when specific members are unavailable, generate
members_unavailablewith member namesTranslation exists but reason is not yet generated by backend or frontend
Include booking limit numbers in reason payloads
When generating
booking_limit_reached_*reasons, includelimit_no(the actual limit value) in the reason payloadApplies to both meeting-level and host-level limits, at both slot and day level
Testing
Unit tests for time_frames_to_slots_service covering:
Slot with multiple reasons returns all in
general_unavailablesMissing date outside range returns
date_range_exceededOverride-available slot is flagged correctly
Day-level reasons collected for booking limits, holidays
Past dates generate
past_timeday-level reasonRound-robin per-member day extraction
Reference Files
app/services/slots/time_frames_to_slots_service.rb(primary)test/services/slots/time_frames_to_slots_service_test.rbPrototype branch: see
mark_available_due_to_override,collect_day_level_reasons,extract_day_level_members,outside_schedulable_range?methods
Phase 2: Frontend Foundation — Calendar Styling + Timezone Warning + Translations
Goal: Visual foundation for the revamped troubleshoot mode. After this phase, the calendar looks correct (grey unavailable days), timezone warning works, and all translation keys are in place.
Scope: CalendarWidget, TimezoneWarningOverlay (new), CalendarWrapper, translations, CSS. No slot-detail changes yet.
Tasks
Calendar day grey styling for unavailable days
Add CSS class
fc-day-troubleshoot-unavailablewith: Background: gray-200, text: gray-600; Hover: gray-500 + white text (0.15s ease transition); Selected + unavailable: gray-500 + whiteIn
CalendarWidget.jsx, detect unavailable days via:isDateFullyUnavailable(slots, date)— backend flag;hasNoAvailableSlots(slots, date)— all slots unavailable; Past datesUnavailable days remain clickable in troubleshoot mode (unlike regular mode)
Timezone warning overlay
Create
TimezoneWarningOverlay.jsxcomponentShows when troubleshoot starts and page timezone != admin's profile timezone
Blur backdrop, centered white card, single "Continue anyway" button (no auto-switch)
Auto-dismisses if user manually changes timezone to match profile
Wire into
CalendarWrapper.jsxstate management
Add all missing translations to en.json
Day-level reason keys:
live.troubleshoot.reasons.day.past_time,day.holiday,day.lead_time,day.date_range_exceeded,day.booking_limit_reached_day/week/month,day.booking_limit_reached_day/week/month_host,day.host_availabilityNew slot-level keys:
host_availability_no_hours,members_unavailable,too_soonUI keys:
dayUnavailableTitle,seeIndividualSlots,hideIndividualSlots,slotAvailableButHasPast,slotAvailableDueToOverrideTimezone warning keys:
timezoneWarning.title,timezoneWarning.description,timezoneWarning.continueAnywaytroubleshootToSeeWhyfor the empty-state link
Slot hover styling
Add theme primary color hover transition (0.15s ease) to slot buttons in troubleshoot mode
Testing
Visual QA: navigate troubleshoot mode across months, verify grey days match spec
Timezone warning: change page timezone to differ from profile, start troubleshoot, verify overlay
Verify all new translation keys render (no missing key warnings in console)
Reference Files
app/javascript/src/components/Live/Calendar/CalendarWidget.jsxapp/javascript/src/components/Live/Calendar/CalendarWrapper.jsxapp/javascript/stylesheets/components/calendar.cssapp/javascript/src/translations/en.jsonPrototype:
TimezoneWarningOverlay.jsx(new file)
Phase 3: Slot-Level Reason Display + Change/View Links
Goal: Each slot shows ALL its unavailability reasons with actionable links. This is the core of the troubleshoot experience for single-host meetings (one-on-one + group).
Scope: TroubleshootSlotDetails.jsx, utils.js helpers, PopoverInfo.jsx.
Tasks
Display all reasons per slot (not just one)
Read from
general_unavailablesarray (Phase 1 backend)Show reasons in the spec's display precedence order: 1. past_time -> 2. date_range_exceeded -> 3. host unavailable (outside availability) -> 4. holiday -> 5. availability override -> 6. lead_time -> 7. meeting/host limits, duration_mismatch, buffer time, booking conflicts, calendar conflicts -> 8. group event full
Each reason: bold title (text-xs font-bold) + grey description (text-xs gray-600)
Change/View links on reasons
-
Add
fixLink(reason, meetingSid)utility that maps reason -> admin page URL:lead_time,date_range_exceeded-> scheduling link settingsbooking_limit_reached_*(meeting) -> scheduling link limit settingsbooking_limit_reached_*_host-> host booking limits pagehost_availability*,duration_mismatch,slot_*_buffer,calendar_event_*_buffer-> host availability page (with specific availability SID when available)holiday-> holidays settingsbooking,booking_*_buffer-> booking detail page (View link)
Add
viewLink(reason)for internal booking reasonsLinks render inline after description text: "...description. [Change]" or "...description. [View]"
Host availability special rendering
Single range: "{hostName} is only available from {start} to {end} as per {availabilityName}."
Multiple ranges: Show centered list of time ranges, then "As per {availabilityName}."
No hours: "{hostName} is not available on this day as per {availabilityName}."
[CHANGE] Override variant: same patterns but "due to an availability override in {availabilityName}" (previously referenced {overrideDate} — updated to use {availabilityName} per the walkthrough video)
Available slot info icons
Past but was available: info icon with "Slot was available, but the time has now passed"
Available due to override: info icon with "This slot is available because of an availability override in {availabilityName}"
Add utility functions to utils.js
reasonI18nKey(reason)— maps reason key to translation keydayReasonI18nKey(reason)— maps day-level reason to translation keyreasonTitle(reason)— returns display titlefixLink(reason, meetingSid)— builds admin page URLviewLink(reason)— builds booking detail URLisInternalBookingReason(reason)— checks if reason is an internal NeetoCal bookingREASON_TITLESconstant map
Testing
Create a scheduling link with various constraints (lead time, date range, buffer times)
Create conflicting bookings and external calendar events
Verify each reason type shows correct title, description, and link
Click change/view links and confirm they navigate to the right admin page
Test
host_availabilitywith 1 range, 3 ranges, and 0 ranges
Reference Files
app/javascript/src/components/Live/Calendar/Slots/TroubleshootSlotDetails.jsxapp/javascript/src/components/Live/Calendar/Slots/PopoverInfo.jsxapp/javascript/src/components/Live/Calendar/utils.jsPrototype: see
REASON_TITLES,fixLink,viewLink,reasonI18nKeyin utils.js
Phase 4: Day-Level "Whole Day Unavailable" Summary
Goal: When all slots on a day are unavailable, show a summary card explaining why — instead of forcing the admin to click individual slots.
Scope: New DayUnavailableSummary.jsx component, updates to TroubleshootSlots.jsx.
Tasks
Create DayUnavailableSummary.jsx
Card with header: "Whole day unavailable because"
Bordered content area listing day-level reasons
Each reason: title + description + change link (same rendering as slot reasons)
Day-level reason keys:
day.past_time,day.holiday,day.lead_time,day.date_range_exceeded,day.booking_limit_reached_*,day.booking_limit_reached_*_host,day.host_availability
[CHANGE] Added
day.past_timeas the highest-priority day-level reason. When the entire day is in the past, show "This day has already passed." with no change link. This prevents users from having to click each slot only to see "past time" everywhere.
"See individual slots" toggle
Below the summary card, show a toggle: "See individual slots" / "Hide individual slots"
When expanded, shows the full slot list below the card
Default state: collapsed (just the summary card)
Detection logic in TroubleshootSlots.jsx
If selected date is in the past -> render DayUnavailableSummary with past_time reason
If selected date has
fullyUnavailable: true-> render DayUnavailableSummaryIf all slots on date are unavailable (no
fullyUnavailableflag but computed) -> also render summaryOtherwise -> render individual slots directly
Day-level reason rendering
Read from
general_unavailablesat the date level (Phase 1 backend)Use
dayReasonI18nKey()for translationsSame change link logic as slot-level
Testing
Navigate to a past date and verify "This day has already passed" banner appears
Set up a holiday and verify "Whole day unavailable" shows with holiday reason + Change link
Set lead time to block an entire day, verify summary card
Set booking limits to block a day, verify card shows the limit type
Set date range to exclude dates, verify card on excluded dates
Toggle "See individual slots" and verify slot list appears/hides
Reference Files
Prototype:
DayUnavailableSummary.jsx(new file)app/javascript/src/components/Live/Calendar/Slots/TroubleshootSlots.jsx
Phase 5: Multi-Member Meetings (Round Robin + Multihost)
Goal: For round-robin and multihost meetings, show per-host breakdown at both day and slot level with availability status, reasons, and avatars.
Scope: DayUnavailableSummary.jsx (multi-member path), PopoverInfo.jsx, HostDayEntry.jsx (new), TroubleshootSlotDetails.jsx (multi-member path).
Tasks
Create HostDayEntry.jsx
Renders a single host's day-level unavailability
Layout: 24px circular avatar + host name (text-xs font-medium gray-800) + reasons below
Reasons always expanded (no accordion)
Each reason: title + description + change link
hostNameandavailabilityNameare per-host (from backend member data)
Multi-member day-level summary (in DayUnavailableSummary.jsx)
Shared reasons shown once at the top (holiday, lead_time, booking limits meeting-level, date_range_exceeded) — NOT repeated per host
"Unavailable hosts" section: Heading with red pill badge: {count}/{total} (error-100 bg, error-600 text); List of HostDayEntry components for each unavailable host
"Available hosts" section: Heading with green pill badge: {count}/{total} (success-100 bg, success-600 text); Just avatar + name, no reasons
Multi-member slot-level popover (in PopoverInfo.jsx)
General reasons (meeting-level blockers) at top with links
Host section ordering depends on slot availability status (see below)
Popover: min-width 400px, max-height 320px with vertical scroll
[CHANGE] Host section ordering for round-robin meetings. Per the walkthrough video, when a slot is available in a round-robin meeting, show "Available hosts" first (on top) and "Unavailable hosts" second (on bottom). When a slot is unavailable, show "Unavailable hosts" first and "Available hosts" second. For multihost meetings, always show "Unavailable hosts" first since the slot is unavailable if any host is blocked.
Hook changes in useSlots.js
For multi-member meetings: extract per-member info from slot data
extractMembersFromSlots()— collects unique members and their availability per slotEnrich
outside_rangereasons with availability range info per member
Meeting type behavior differences
Round-robin: slot available when ANY host is free; day unavailable when ALL hosts blocked for all slots; available slot still shows per-host info icon with reasons for unavailable hosts
Multihost: slot available when ALL hosts are free; day unavailable when ANY host is blocked for all slots
Testing
Create a round-robin meeting with 3+ hosts
Set different availabilities per host, verify per-host breakdown
Set a holiday that blocks all hosts, verify shared reason shown once (not per host)
Set host-level booking limits, verify
_hostvariant reasons per memberCreate a multihost meeting, verify different availability logic
Verify available round-robin slots show available hosts first, unavailable hosts second
Verify unavailable slots show unavailable hosts first, available hosts second
Test slot-level popover scrolling with many hosts
Reference Files
Prototype:
HostDayEntry.jsx(new file),PopoverInfo.jsx(enhanced),DayUnavailableSummary.jsx(multi-member path)app/javascript/src/hooks/slots/useSlots.jsapp/javascript/src/components/Live/Calendar/utils.js(extractMembersFromSlots,groupedMembers)
Phase Dependencies
Phase 1 is a prerequisite for all others (backend must expose the data)
Phase 2 can run in parallel with Phase 3 (independent UI concerns) — these are quick wins that should be shipped fast
Phase 3 must complete before Phase 4 (day summary reuses slot reason rendering)
Phase 4 must complete before Phase 5 (multi-member extends the day summary)
Phase 1 (Backend) ──> Phase 2 (Calendar + TZ + Translations) ──> Phase 3 (Slot Reasons + Links)
|
Phase 4 (Day Summary)
|
Phase 5 (Multi-Member)
Prototype Reference
The troubleshoot-revamp-functional branch contains a working prototype of all 5 phases. Engineers should use it as a visual reference and for understanding data flow, but should not merge it directly — it needs to be rewritten cleanly in each phase's PR.
Key prototype files to reference:
Backend:
app/services/slots/time_frames_to_slots_service.rb(see new methods)Frontend utilities:
app/javascript/src/components/Live/Calendar/utils.js(see new helpers)New components:
DayUnavailableSummary.jsx,HostDayEntry.jsx,TimezoneWarningOverlay.jsxEnhanced:
TroubleshootSlotDetails.jsx,PopoverInfo.jsx,CalendarWidget.jsx