Add design spec for Nextcloud CalDAV calendar sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
71717cccc1
commit
43395235e2
1 changed files with 139 additions and 0 deletions
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Nextcloud Calendar Sync — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-04-10
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add a second calendar sync target (Nextcloud CalDAV) alongside the existing Google Calendar sync. Shared logic is extracted into a common interface and a static helper, keeping both service implementations independent and self-contained.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Shared interface: `ICalendarService`
|
||||||
|
|
||||||
|
Replaces `IGoogleCalendarService`. Both providers implement this interface:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ICalendarService
|
||||||
|
{
|
||||||
|
Task<List<CalendarInfo>> GetCalendarsAsync();
|
||||||
|
Task SyncShiftsToCalendarAsync(string calendarId, List<ShiftWithTransport> shifts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CalendarInfo
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared helper: `CalendarEventBuilder` (static class)
|
||||||
|
|
||||||
|
Extracts pure data-transformation logic currently inside `GoogleCalendarService`:
|
||||||
|
|
||||||
|
- `FormatTransportInfo(TransportConnection)` → string
|
||||||
|
- `BuildEventContent(Shift, DateTimeOffset start, DateTimeOffset end, TransportConnection?)` → returns a shared `CalendarEventData` record (summary + description)
|
||||||
|
|
||||||
|
No orchestration logic lives here — just pure functions with no side effects.
|
||||||
|
|
||||||
|
### `GoogleCalendarService`
|
||||||
|
|
||||||
|
- Implements `ICalendarService`
|
||||||
|
- Refactored to use `CalendarEventBuilder` for event content
|
||||||
|
- All Google client library calls remain here
|
||||||
|
- `GetCalendarsAsync()` returns `List<CalendarInfo>` mapped from Google's `CalendarListEntry`
|
||||||
|
- Identification of owned events via `ExtendedProperties.Private["shiftSchedulerEvent"]` unchanged
|
||||||
|
|
||||||
|
### `NextcloudCalendarService`
|
||||||
|
|
||||||
|
- Implements `ICalendarService`
|
||||||
|
- Uses `HttpClient` with Basic Auth (username + app password from configuration)
|
||||||
|
- CalDAV operations:
|
||||||
|
- **List calendars**: `PROPFIND` on `/remote.php/dav/calendars/<username>/`, depth 1, parse `displayname` and `resourcetype`
|
||||||
|
- **Delete existing events in range**: `REPORT` (CalDAV `calendar-query`) to find VEVENTs with `X-SHIFT-SCHEDULER:true` property in the date range, then `DELETE` each by URL
|
||||||
|
- **Create events**: `PUT` a VEVENT (iCalendar format using the existing `Ical.Net` dependency) to `<calendar-url>/<uid>.ics`. Each event includes `X-SHIFT-SCHEDULER:true` as a custom property for future identification.
|
||||||
|
- UIDs are generated deterministically: `shift-<shiftName>-<dateISO>-<morning|afternoon>` to allow idempotent re-sync if needed
|
||||||
|
- Retry logic mirrors Google's exponential backoff, extracted from `GoogleCalendarService` into a shared `RetryHelper` static method in `CalendarEventBuilder`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
|
||||||
|
`ShiftController` is updated:
|
||||||
|
|
||||||
|
- Both `IGoogleCalendarService` and `INextcloudCalendarService` injected. These are empty marker interfaces that extend `ICalendarService`, registered with their concrete types in DI. This avoids DI ambiguity while keeping the shared interface as the contract:
|
||||||
|
```csharp
|
||||||
|
public interface IGoogleCalendarService : ICalendarService { }
|
||||||
|
public interface INextcloudCalendarService : ICalendarService { }
|
||||||
|
```
|
||||||
|
- Old endpoints `GET /api/shift/google_calendars` and `POST /api/shift/sync_to_google_calendar` are **replaced** by:
|
||||||
|
- `GET /api/shift/calendars?provider=google|nextcloud` → `List<CalendarInfo>`
|
||||||
|
- `POST /api/shift/sync_to_calendar` → body: `{ Provider, CalendarId, Year, Month }`
|
||||||
|
- Provider routing via a `switch` on the `provider` string — no factory abstraction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client UI
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
Two separate sync buttons in `Index.razor`:
|
||||||
|
- **Sync to Google Calendar** (existing, unchanged label)
|
||||||
|
- **Sync to Nextcloud** (new)
|
||||||
|
|
||||||
|
Both buttons are disabled while `_isSyncing` is true.
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
New field `_syncProvider` (`"google"` or `"nextcloud"`) tracks which provider triggered the current flow.
|
||||||
|
|
||||||
|
Existing `_availableCalendars`, `_showCalendarSelector`, and the calendar picker modal are reused for both providers. The modal title changes to "Select Google Calendar" or "Select Nextcloud Calendar" based on `_syncProvider`.
|
||||||
|
|
||||||
|
### Client DTO
|
||||||
|
|
||||||
|
`GoogleCalendar` record on the client is renamed to `CalendarInfo` to match the server DTO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### `appsettings.json` (placeholder — committed to git)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Nextcloud": {
|
||||||
|
"BaseUrl": "https://your-nextcloud-instance.example.com",
|
||||||
|
"Username": "YOUR_NEXTCLOUD_USERNAME",
|
||||||
|
"AppPassword": "YOUR_NEXTCLOUD_APP_PASSWORD"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production appsettings (not committed, mounted via Docker)
|
||||||
|
|
||||||
|
Contains real values for `BaseUrl`, `Username`, and `AppPassword`.
|
||||||
|
|
||||||
|
Read in `Program.cs` via `IConfiguration`, injected into `NextcloudCalendarService` — same pattern as `Authentication:Google`.
|
||||||
|
|
||||||
|
### `DOCKER_CONFIG.md`
|
||||||
|
|
||||||
|
Add a note that the `Nextcloud` section must be populated in the production `appsettings.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
No new NuGet packages required:
|
||||||
|
- CalDAV HTTP calls: plain `HttpClient`
|
||||||
|
- iCalendar VEVENT generation: `Ical.Net` (already in `ShiftScheduler.Services.csproj`)
|
||||||
|
- XML parsing for CalDAV responses: `System.Xml` (built-in)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Two-way sync (read events from calendar back into the scheduler)
|
||||||
|
- Nextcloud OAuth — app password is sufficient for a personal instance
|
||||||
|
- Caching of calendar lists
|
||||||
Loading…
Reference in a new issue