feat(scheduling): dezky Scheduling — Calendly-style booking on Stalwart calendars
First-party booking system on top of Stalwart calendars (no third-party scheduling dependency). Hosts expose public booking pages; visitors pick a slot computed from the host's live Stalwart free/busy, and confirming writes the event to the host's calendar and sends a dezky-branded confirmation with an .ics. platform-api (services/platform-api/src/scheduling): - Schemas: Host, StalwartCredential (AES-256-GCM at rest), AvailabilitySchedule, EventType, Booking, SlotLock (unique (hostId,startUtc) + TTL). - StalwartCalendarModule: JMAP gateway (free/busy via Principal/getAvailability, event create/delete, scheduleAgent=client) + on-behalf app-password provisioning. CredentialCipher for at-rest encryption. - DST-correct slot engine (Luxon) with unit tests; two-layer double-booking guard (atomic SlotLock + live free/busy re-check). - Booking confirm/cancel/reschedule, branded email + .ics via JMAP submission, self-service manage tokens. /api/v1 public + tenant-gated admin routes, per-IP rate limiting. apps/booking: standalone public, whitelabel booking app (booking.dezky.eu) — path-based tenant resolution, per-tenant brand colour, booking + manage flows. apps/portal: admin scheduling page (hosts, event types, availability, bookings with edit/delete + admin cancel/reschedule) and proxy routes. infra: booking dev service in docker-compose; scheduling env vars.
This commit is contained in:
@@ -62,6 +62,7 @@ const ADMIN_NAV: NavRow[] = [
|
||||
{ id: 'users', label: 'Users & groups', icon: 'users', href: '/admin/users' },
|
||||
{ id: 'mail', label: 'Mail settings', icon: 'mail', href: '/admin/mail' },
|
||||
{ id: 'meetings', label: 'Meetings', icon: 'video', href: '/admin/meetings' },
|
||||
{ id: 'scheduling', label: 'Scheduling', icon: 'calendar', href: '/admin/scheduling' },
|
||||
{ id: 'chat', label: 'Chat', icon: 'chat', href: '/admin/chat' },
|
||||
{ id: 'domains', label: 'Domains', icon: 'globe', href: '/admin/domains' },
|
||||
{ id: 'storage', label: 'Storage', icon: 'database', href: '/admin/storage' },
|
||||
@@ -121,6 +122,7 @@ const currentId = computed(() => {
|
||||
if (p.startsWith('/admin/users')) return 'users'
|
||||
if (p.startsWith('/admin/mail')) return 'mail'
|
||||
if (p.startsWith('/admin/meetings')) return 'meetings'
|
||||
if (p.startsWith('/admin/scheduling')) return 'scheduling'
|
||||
if (p.startsWith('/admin/chat')) return 'chat'
|
||||
if (p.startsWith('/admin/domains')) return 'domains'
|
||||
if (p.startsWith('/admin/storage')) return 'storage'
|
||||
|
||||
Reference in New Issue
Block a user