# Build a personal Calendar client ## Read first Read [the shared guide](https://wspc.ai/llms.txt), [Auth OpenAPI](https://api.wspc.ai/auth/openapi.json), and [calendar OpenAPI](https://api.wspc.ai/calendar/openapi.json) before you generate code. Use live request schemas, response schemas, examples, and descriptions. Check HTTP status and content; an error page is not a schema. If you cannot read a required document, report the access problem instead of inventing an API. ## Interview the user Ask one main question about how they plan their days. Offer these examples: today at a glance, a weekly plan, or work across time zones. Allow a free-text answer. Ask at most one follow-up if it changes the result. Use an answer already given instead of asking again. Do not ask the user to choose endpoints or data models. Map their answer to the default date range, information density, and time-zone display. ## Build and deliver Deliver a downloadable single HTML file with vanilla JS, inline styles, no build step, no backend, and no client secret. Use a small in-memory state and explicit forms. Give it a clear visual hierarchy, readable spacing, responsive layout, visible keyboard focus, labels, and status messages announced to assistive tools. Use native buttons and form controls. Render user content with textContent. Implement the full domain scope below. Show sign-in, waiting for approval, loading, empty data, success, and distinct error states. Check every HTTP status before reading a success field. A failed request must not look like an empty list or a successful write. Keep drafts and selected File objects on failure, including refresh failure. Disable duplicate submissions while a write is in flight. Unknown write outcomes require readback and a new user decision, not blind retry. After a confirmed write, update state from the response or explicitly reload the affected list. Keep a successful write distinct from a failed subsequent reload. Use per-database opaque consistency bookmarks: x-cb-auth and x-cb-cal. Save response headers in memory and send each unchanged on subsequent requests to its own domain. Never parse, compare, log, or put bookmarks in JSON payloads. Reset domain state when the user changes account. Keep pagination cursor state separate from bookmarks and form confirmations. Show network failure, authentication required, validation failure, conflict, rate limit, and service failure separately. Domain errors use { error: { code, message } }; OAuth errors use { error, error_description }. For conflicts, retain the original input and confirmation. Offer an explicit reload and review action; never fetch a newer version and resend automatically. Use a request timeout and preserve input when it fires. Do not log credentials. Give the user the actual .html file, not just code in chat. Tell them to download it and open it in a real browser; chat previews may block network or OAuth. Before delivery, check keyboard actions, empty data, reads, writes, and error states with authorized fixtures. Report any behavior you could not verify. ## OAuth for a file opened from `file://` Use device flow for this local-file client. A hosted browser SPA should use authorization code + PKCE from the shared guide, with an exact registered redirect URI and state validation. The resource is https://api.wspc.ai and the available scope is wspc:full. Explain that this is broad Workspace access, not a calendar-only permission. No backend or secret is needed. Send JSON bodies to https://api.wspc.ai for the following requests: 1. Register once with POST /auth/oauth/register: { "client_name": "My calendar client", "redirect_uris": ["http://localhost"], "grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"], "token_endpoint_auth_method": "none" }. Cache the returned client_id in localStorage and reuse it after reload. 2. POST /auth/oauth/device with { "client_id": "", "scope": "wspc:full", "resource": "https://api.wspc.ai" }. Show a user-clickable link to verification_uri_complete for browser approval. Use the returned URL; do not ask the user to copy a code unnecessarily. 3. Poll POST /auth/oauth/token with { "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "", "client_id": "" }. Wait at least the returned interval before each poll. authorization_pending continues; slow_down adds five seconds to the interval. Use recursive setTimeout with the current interval. Stop on access_denied or expired_token and show the correct state with an explicit restart action. Bound polling with expires_in; cancel an old poll when starting another login. 4. Save access_token, refresh_token, and expiry together before using the new access token. On expiry or one 401, use a single shared refresh promise: POST /auth/oauth/token with { "grant_type": "refresh_token", "refresh_token": "", "client_id": "" }. Save the new pair before releasing waiting calls. Token Family Rotation means old tokens can become Consumed; never reuse an old pair after rotation. If refresh fails, clear invalid credentials and require sign-in, preserving drafts. Do not run independent refreshes for concurrent API calls. API calls send Authorization: Bearer . Tokens stay in browser localStorage, with an inline comment explaining its XSS exposure. Keep them out of URLs, prompts, logs, downloads, and screenshots. Sign-out revokes the Token Family with the live revoke endpoint and clears credentials. If revocation fails, report it and clear local credentials. Retain only the reusable public client_id. Use expiry returned by the server rather than a hard-coded token lifetime. ## Calendar scope and time Build a Calendar Agenda with editable date range and Agenda View Time Zone. GET /calendar/agenda requires bounded start and end as ISO date-times with offset, and view_time_zone as an IANA zone. Use a short initial range. Read items in server order and follow next_cursor or provide Load more. Keep the same range and zone while paginating; reset the cursor when either changes. Handle cursor errors as a request to reload the view, preserving open drafts. An Agenda Item is a projection, not an editable Event row: - kind: single uses event_id. Read GET /calendar/events/{id} before offering edit or soft-delete. The detail is the Event object, not an event wrapper. - kind: occurrence uses series_id and recurrence_id. Show it as a read-only Occurrence. Never send its actions to the Series Master. - Read GET /auth/me and compare the Event user_id with the signed-in user id. Only enable writes when detail proves ownership, an empty attendees array, no recurrence_rule, and an active single Event. Missing proof means read-only. Label existing Events with attendees and other owners as read-only too. Create with POST /calendar/events. Include title, start, and end; send no attendees or recurrence. The server derives all_day from date-only inputs. PATCH /calendar/events/{id} sends expected_version from the detail and only intentionally changed fields. DELETE at the same path sends a JSON body with expected_version. Preserve the confirmed version for both operations. After a 409 VERSION_CONFLICT, require a fresh user review; do not silently replace it. Use Luxon DateTime for parsing, formatting, arithmetic, and time-zone conversion. Embed one verified version of its browser distribution in the HTML, preserving its license, so the downloaded file remains self-contained. Use ISO 8601 with offset for timed writes. Use ISO date-only for all-day start and exclusive end: a one-day item on 2026-09-09 ends on 2026-09-10. Display all-day date components without converting them to another zone. Validate that end is later than start and that both are the same kind. Keep original timed values for untouched fields; a display-zone change must not change the stored instant. Offer timed and all-day create, edit, and soft-delete forms. Explain exclusive end in the form or convert an inclusive UI end with Luxon. Show a zone selector for timed input. Announce invalid dates or zones and preserve the form. Recurring Series changes, Occurrence changes, invites, RSVP, ICS, and drag-and-drop scheduling are outside this client. Do not provide attendee input or send invites.