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:
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
// Root of booking.dezky.eu carries no tenant context — booking links are always
|
||||
// /:tenantSlug/:hostSlug/:eventTypeSlug. Show a neutral placeholder.
|
||||
useHead({ title: 'dezky Scheduling' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="wrap">
|
||||
<div class="card">
|
||||
<div class="brand">dezky · scheduling</div>
|
||||
<h1>Booking</h1>
|
||||
<p class="mute">Open the specific booking link you were given to choose a time.</p>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 16px; }
|
||||
.card { max-width: 460px; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 40px 28px; }
|
||||
.brand { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-mute); }
|
||||
h1 { font-size: 24px; margin: 8px 0; }
|
||||
.mute { color: var(--text-mute); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user