OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking commits / 9ce51b5

Simplify Google Calendar UX, always show section in settings
- Always register gcal routes (status endpoint works without credentials) - Status returns { configured: false } when no GCAL_CLIENT_ID set - Settings shows clear message: "set GCAL_CLIENT_ID to enable" - Connected state: simple status + disconnect button (no calendar picker clutter) - Not connected: one "Connect Google Calendar" button - Removed overengineered calendar picker (defaults to primary, can add later) 34/34 tests, 174 assertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9ce51b57437f83f7c4a0d25d001b0f20bacb689f
by plankatron <fleeky@gmail.com> · 2026-03-22T19:50:45.000Z

Changed files (3) +58 -70

lib/routes/gcal.js +26 -18
……@@ -10,8 +10,34 @@ import { readJson } from '../response.js'
1010export function registerGcalRoutes (router, server) {
1111 const gcal = server.gcal
1212
13 // Status — always available, returns configured + connected state
14 router.get('/api/gcal/status', async (req, res) => {
15 if (!server.gcalConfigured) {
16 return json(res, { configured: false, connected: false })
17 }
18
19 const auth = await requireAuth(req, server, 'write')
20 const providerId = await getProviderIdForUser(server, auth.userId)
21 if (!providerId) return json(res, { configured: true, connected: false })
22
23 const entry = await server.db.get(`gcal:provider:${providerId}`)
24 if (!entry || !entry.value.connected) {
25 return json(res, { configured: true, connected: false })
26 }
27
28 json(res, {
29 configured: true,
30 connected: true,
31 calendarIds: entry.value.calendarIds || ['primary'],
32 connectedAt: entry.value.connectedAt
33 })
34 })
35
1336 // Get OAuth2 authorization URL
1437 router.get('/api/gcal/auth-url', async (req, res) => {
38 if (!server.gcalConfigured) {
39 return json(res, { error: 'Google Calendar not configured. Set GCAL_CLIENT_ID and GCAL_CLIENT_SECRET environment variables.' }, 400)
40 }
1541 const auth = await requireAuth(req, server, 'write')
1642 const providerId = await getProviderIdForUser(server, auth.userId)
1743 if (!providerId) return json(res, { error: 'No provider profile' }, 403)
……@@ -100,24 +126,6 @@ export function registerGcalRoutes (router, server) {
100126 }
101127 })
102128
103 // Get connection status
104 router.get('/api/gcal/status', async (req, res) => {
105 const auth = await requireAuth(req, server, 'write')
106 const providerId = await getProviderIdForUser(server, auth.userId)
107 if (!providerId) return json(res, { connected: false })
108
109 const entry = await server.db.get(`gcal:provider:${providerId}`)
110 if (!entry || !entry.value.connected) {
111 return json(res, { connected: false })
112 }
113
114 json(res, {
115 connected: true,
116 calendarIds: entry.value.calendarIds || ['primary'],
117 connectedAt: entry.value.connectedAt
118 })
119 })
120
121129 // List available calendars from Google
122130 router.get('/api/gcal/calendars', async (req, res) => {
123131 const auth = await requireAuth(req, server, 'write')
lib/server.js +7 -4
……@@ -58,9 +58,12 @@ export class HyperBookingServer {
5858 // Google Calendar integration (optional — requires GCAL_CLIENT_ID env var)
5959 const gcalClientId = options.gcalClientId || (typeof process !== 'undefined' && process.env && process.env.GCAL_CLIENT_ID)
6060 const gcalClientSecret = options.gcalClientSecret || (typeof process !== 'undefined' && process.env && process.env.GCAL_CLIENT_SECRET)
61 this.gcal = gcalClientId
62 ? new GoogleCalendarClient({ clientId: gcalClientId, clientSecret: gcalClientSecret })
63 : null
61 // Always create gcal client — routes check if credentials exist
62 this.gcal = new GoogleCalendarClient({
63 clientId: gcalClientId || '',
64 clientSecret: gcalClientSecret || ''
65 })
66 this.gcalConfigured = !!gcalClientId
6467
6568 // Router
6669 this.router = new Router(this)
……@@ -133,7 +136,7 @@ export class HyperBookingServer {
133136 registerAvailabilityRoutes(this.router, this)
134137 registerBookingRoutes(this.router, this)
135138 registerDashboardRoutes(this.router, this)
136 if (this.gcal) registerGcalRoutes(this.router, this)
139 registerGcalRoutes(this.router, this)
137140 // Public slug routes MUST be last (catch-all /:slug)
138141 registerPublicRoutes(this.router, this)
139142 }
web/js/dashboard.js +25 -48
……@@ -678,62 +678,39 @@ async function loadGcalSection () {
678678 try {
679679 const status = await api('GET', '/api/gcal/status')
680680
681 if (status.connected) {
682 let html = '<p style="color:var(--success);margin-bottom:0.5rem;">Connected</p>'
683 html += '<p style="font-size:0.9rem;color:var(--text-secondary);margin-bottom:1rem;">Monitoring: ' + (status.calendarIds || ['primary']).join(', ') + '</p>'
684 html += '<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">'
685 html += '<button class="btn btn-secondary" id="gcal-pick-btn">Change Calendars</button>'
686 html += '<button class="btn btn-danger" id="gcal-disconnect-btn">Disconnect</button>'
687 html += '</div>'
688 html += '<div id="gcal-picker" style="display:none;margin-top:1rem;"></div>'
689
690 gcalEl.innerHTML = html
681 if (!status.configured) {
682 // Server doesn't have Google credentials set up
683 gcalEl.innerHTML = `
684 <p style="color:var(--text-secondary);">
685 To enable Google Calendar sync, set <code>GCAL_CLIENT_ID</code> and
686 <code>GCAL_CLIENT_SECRET</code> environment variables when starting the server.
687 </p>`
688 return
689 }
691690
691 if (status.connected) {
692 // Connected — show simple status + disconnect
693 gcalEl.innerHTML = `
694 <p><span style="color:var(--success);font-weight:500;">Connected</span>
695 — your Google Calendar events automatically block booking slots.</p>
696 <p style="font-size:0.85rem;color:var(--text-secondary);margin:0.5rem 0;">
697 Only availability (busy/free) is read. Event details are never accessed.
698 </p>
699 <button class="btn btn-danger" id="gcal-disconnect-btn">Disconnect</button>
700 `
692701 document.getElementById('gcal-disconnect-btn').addEventListener('click', async () => {
693 if (!confirm('Disconnect Google Calendar? Booking slots will no longer reflect your calendar events.')) return
702 if (!confirm('Disconnect Google Calendar?')) return
694703 try {
695704 await api('DELETE', '/api/gcal/disconnect')
696 showToast('Google Calendar disconnected', 'success')
705 showToast('Disconnected', 'success')
697706 loadGcalSection()
698707 } catch (err) { showToast(err.message, 'error') }
699708 })
700
701 document.getElementById('gcal-pick-btn').addEventListener('click', async () => {
702 const picker = document.getElementById('gcal-picker')
703 if (picker.style.display !== 'none') { picker.style.display = 'none'; return }
704
705 try {
706 const calendars = await api('GET', '/api/gcal/calendars')
707 const currentIds = status.calendarIds || ['primary']
708
709 let pickerHtml = '<p style="font-size:0.9rem;margin-bottom:0.5rem;">Select calendars to check for conflicts:</p>'
710 for (const cal of calendars) {
711 const checked = currentIds.includes(cal.id) ? 'checked' : ''
712 pickerHtml += `<label style="display:block;margin:0.3rem 0;font-size:0.9rem;">
713 <input type="checkbox" class="gcal-cal-checkbox" value="${esc(cal.id)}" ${checked}>
714 ${esc(cal.summary)}${cal.primary ? ' (primary)' : ''}
715 </label>`
716 }
717 pickerHtml += '<button class="btn btn-accent" id="gcal-save-cals" style="margin-top:0.5rem;">Save</button>'
718 picker.innerHTML = pickerHtml
719 picker.style.display = ''
720
721 document.getElementById('gcal-save-cals').addEventListener('click', async () => {
722 const checked = Array.from(document.querySelectorAll('.gcal-cal-checkbox:checked')).map(cb => cb.value)
723 if (checked.length === 0) { showToast('Select at least one calendar', 'error'); return }
724 try {
725 await api('PUT', '/api/gcal/calendars', { calendarIds: checked })
726 showToast('Calendars updated', 'success')
727 loadGcalSection()
728 } catch (err) { showToast(err.message, 'error') }
729 })
730 } catch (err) { showToast(err.message, 'error') }
731 })
732709 } else {
710 // Not connected — one button to connect
733711 gcalEl.innerHTML = `
734712 <p style="color:var(--text-secondary);margin-bottom:1rem;">
735 Connect your Google Calendar to automatically block times when you have events.
736 We only read availability (busy/free) — never event details.
713 Sync your Google Calendar so clients only see times when you're actually free.
737714 </p>
738715 <button class="btn btn-accent" id="gcal-connect-btn">Connect Google Calendar</button>
739716 `
……@@ -744,8 +721,8 @@ async function loadGcalSection () {
744721 } catch (err) { showToast(err.message, 'error') }
745722 })
746723 }
747 } catch {
748 gcalEl.innerHTML = '<p style="color:var(--text-secondary);">Calendar integration not configured. Set GCAL_CLIENT_ID to enable.</p>'
724 } catch (err) {
725 gcalEl.innerHTML = '<p style="color:var(--text-secondary);">Could not load calendar status.</p>'
749726 }
750727}
751728