- TypeScript 54.5%
- Vue 43.6%
- JavaScript 1.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo | ||
| config | ||
| docs/screenshots | ||
| src | ||
| tests/unit | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .renovaterc.json | ||
| docker-compose.yml | ||
| eslint.config.js | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
Warning: This project is fully vibe coded. Use at your own risk.
Calendar for OpenCloud
A web app for OpenCloud that reads and manages calendars and appointments from the OpenCloud
CalDAV endpoint (/caldav/). It is based on
web-app-skeleton.
Table of contents
Screenshots
Calendar
Global search
Calendar settings
Build and install
pnpm build
Copy the contents of dist/ into $OC_DATA_DIR/web/assets/apps/calendar and restart
OpenCloud. The generated manifest.json is used for app discovery.
Prebuilt packages
.forgejo/workflows/build.yml runs the tests, type checks, lint, and build on every push and
pull request, then packages the result as opencloud-calendar-<version>.zip.
Every push to master replaces a rolling Latest development build prerelease (latest
tag) with the new zip and SHA256SUMS. Pushing a v* tag publishes a separate versioned
release instead. Both are downloadable from the repository's Releases page.
The archive contains a top-level calendar/ directory, so it unpacks directly into the apps
folder:
unzip opencloud-calendar-<version>.zip -d "$OC_DATA_DIR/web/assets/apps"
Then restart OpenCloud (for example systemctl restart opencloud, or restart the OpenCloud
container). The workflow expects a Forgejo runner with the docker label.
Features
- CalDAV discovery through
current-user-principalandcalendar-home-set - Calendar list with colors, visibility toggles, and appointment counts
- Month grid with previous/next/today navigation
- Day agenda with appointment details (time, location, description, calendar)
- Create, edit, move, and delete appointments, including all-day and multi-day events
- Event dialog with title, calendar, all-day/time controls, location, description, and repeat rules
- Simple recurrence (daily, weekly, monthly, yearly) with an interval and an end by count or date
- Whole-series editing for recurring appointments; existing recurrence exceptions are preserved
- Recurring events, exceptions, all-day events, and multi-day events
- Drag non-recurring appointments onto another day to reschedule them
- Create calendars, edit their name, color, and description, and delete them
- Per-calendar option to include or exclude appointments from global search
- Calendar events in OpenCloud's global search (title, description, and location), including a full results page and links that open the appointment in the calendar
- Partial-failure handling: one inaccessible calendar does not hide the others
- Calendar creation via
MKCALENDAR, property updates viaPROPPATCH, and deletion viaDELETE - Appointment mutations via conditional
PUT/DELETE(If-Match/If-None-Match)
The app uses the access token from the OpenCloud session (useAuthStore) and sends it as a
Bearer token. Authentication is terminated by the OpenCloud proxy, which forwards the
authenticated user to Radicale (or another CalDAV backend).
Architecture
src/
caldav/
client.ts CalDAV transport, discovery, event queries, calendar/event mutations
xml.ts PROPFIND/REPORT/MKCALENDAR/PROPPATCH builders and multistatus parsing
ics.ts iCalendar parser and recurrence expansion
generate.ts VEVENT/RRULE generation, series updates, event-to-input conversion
search/
provider.ts Global search provider (preview and full list)
calendarCache.ts Cached calendars for search
types.ts Search result item and type guard
components/ Sidebar, month grid, day agenda, dialogs, color picker, search views
utils/color.ts Hex/HSV conversion for the in-app color picker
utils/date.ts Local-time date and month-grid helpers
utils/link.ts Appointment deep-link query helpers
utils/search.ts Search term extraction from KQL
views/ Calendar page
types.ts Domain model shared by every layer
The layers are independent:
caldav/knows HTTP, DAV XML, and iCalendar. It does not import Vue or UI code.CalendarViewowns asynchronous state and exposes data plus actions to its children.- Components receive data through props and emit intent. They do not call the network.
types.tsis the shared contract. Add a new field there, populate it inics.ts, then render it in a component.
CalendarEvent keeps icsData (the original VCALENDAR), and parseCalendarObject expands
recurrence without discarding exceptions. updateEventIcs rewrites the master VEVENT and
preserves exceptions, so series edits and moves keep the rest of the calendar data intact.
Adding a new mutation follows the same pattern: add the conditional DAV call to CalDavClient
and reuse the serializer, without replacing the parser.
Development
Requirements:
- Node.js 22 or newer
- pnpm (use the version from the
packageManagerfield) - Docker and Docker Compose
Install dependencies and start the watcher:
pnpm install
pnpm build:w
Start the development stack in another terminal:
docker compose up
Open https://host.docker.internal:9200 and sign in with admin / admin. The built app is
mounted from ./dist into /web/apps/calendar and is discovered automatically. The included
Radicale service creates a Personal Calendar on first access.
pnpm build:w needs to be running before the app can be loaded. A browser reload is required
after a rebuild.
The stack publishes port 9200, so stop any other OpenCloud dev stack before docker compose up. To deploy into an existing instance instead, copy dist/ to
$OC_DATA_DIR/web/assets/apps/calendar and restart OpenCloud.
CalDAV backend
The development setup routes CalDAV through OpenCloud. For an existing installation, add the following routes to the OpenCloud proxy configuration and point them at your CalDAV server:
additional_policies:
- name: default
routes:
- endpoint: /caldav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /.well-known/caldav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
The app talks to /caldav/ on the same origin as OpenCloud, so no additional CSP or CORS
configuration is required.
Testing
pnpm test:unit
pnpm check:types
pnpm lint
The unit tests cover CalDAV XML parsing, iCalendar parsing (all-day and floating times,
recurrence expansion, EXDATE, exceptions, cancellation, malformed data), ICS generation and
recurrence rules (escaping, exclusive all-day ends, timezone cleanup, series updates), the
authenticated CalDAV client (discovery, search, calendar and event mutations, ETag and 412
handling, move ordering, failure isolation, error mapping), the global search provider (KQL
extraction, result mapping, source filtering, caching, limit handling), and the color, date,
and deep-link helpers.


