- TypeScript 56.9%
- Vue 41.4%
- JavaScript 1.7%
| 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 | ||
Note: This project is fully vibe coded. Use at your own risk.
Contacts for OpenCloud
A web app for OpenCloud that browses address books and contacts from the OpenCloud CardDAV
endpoint (/carddav/). It is based on
web-app-skeleton.
Table of contents
Screenshots
Contacts
Editing a contact
Profile picture editor
Global search
Build and install
pnpm build
Copy the contents of dist/ into $OC_DATA_DIR/web/assets/apps/contacts 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-contacts-<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 contacts/ directory, so it unpacks directly into the apps
folder:
unzip opencloud-contacts-<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
- CardDAV discovery through
current-user-principalandaddressbook-home-set - Address book sidebar with descriptions and contact counts
- Contact list grouped alphabetically, with generated avatars and optional photos
- Search across names, companies, email addresses, phone numbers, addresses, and notes
- Global OpenCloud search provider with a topbar preview and full result list; results deep-link directly to the contact
- Category sidebar with per-category counts and single-category filtering
- Contact details for email, phone, postal addresses, websites, organization, title, role, birthday, anniversary, categories, notes, and group membership
- Create contacts in the selected address book, and edit their names, organization, contact methods, addresses, websites, dates, categories, and notes
- Upload, crop, replace, or remove contact photos; choose a square with pan and zoom, then the image is compressed into a 512px JPEG before being embedded in the vCard
- Pill-based category editor with autocomplete for existing categories; type and press comma to add a new one
- Email and phone categories based on vCard types: Private (
HOME), Office (WORK), Mobile (CELL), Fax, Pager, and Other, with existing custom types preserved - Conditional
PUTupdates (If-Match) that reject concurrent changes, and guarded creates (If-None-Match: *) that never overwrite an existing contact - vCard fields the editor does not expose are preserved, including groups and custom
X-properties - Apple-style custom labels (
X-ABLabel) and preferred values (PREF) - vCard 3.0 and 4.0 parsing, including folded lines, quoted-printable values, base64 photos,
data URI photos, and groups (
KIND:group) - Partial-failure handling: one inaccessible address book does not hide the others
- Creating and deleting contacts is not implemented yet
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 CardDAV backend).
Architecture
src/
carddav/
client.ts CardDAV transport, discovery, address book queries, updates
xml.ts PROPFIND/REPORT builders and multistatus parsing
vcard.ts vCard parser and contact mapping
generate.ts Editable-field mapping and vCard rewriting
composables/
useContacts.ts Address book/contact state, loading, selection
search/
provider.ts Global search provider (preview and full list)
contactCache.ts Cached address books and contacts for search
types.ts Search result item and type guard
components/ Sidebar, contact list, contact details, avatar, search views
utils/contact.ts Search, sorting, grouping, labels, link/date formatting
utils/image.ts Image decoding, square cropping, and JPEG compression
utils/link.ts Contact deep link query helpers
utils/search.ts KQL search term extraction
views/ Contacts page
types.ts Domain model shared by every layer
The layers are independent:
carddav/knows HTTP, DAV XML, and vCard. It does not import Vue or UI code.useContactsowns asynchronous state and exposes data plus actions.- 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 invcard.ts, then render it in a component.
Contact keeps raw (the original vCard), and parseVCard returns every property, including
unknown X- properties. updateContactVCard rewrites only the editable properties and
preserves the rest, so photos, groups, custom fields, and future features survive an edit.
Adding create or delete follows the same pattern: add the conditional DAV call to
CardDavClient 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/contacts and is discovered automatically. The included
Radicale service creates a Personal Address Book 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/contacts and restart OpenCloud.
CardDAV backend
The development setup routes CardDAV through OpenCloud. For an existing installation, add the following routes to the OpenCloud proxy configuration and point them at your CardDAV server:
additional_policies:
- name: default
routes:
- endpoint: /carddav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
- endpoint: /.well-known/carddav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
The app talks to /carddav/ 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 CardDAV XML parsing, vCard parsing (folding, escapes, typed values, custom labels, photos, quoted-printable, vCard 4 groups), vCard rewriting and field preservation and photo add/replace/remove, the authenticated CardDAV client (discovery, query, update, failure isolation, error mapping), the global search provider (KQL extraction, result mapping, source filtering, caching, de-duplication, partial failures), and contact search/sorting, category counting/filtering, and label helpers.



