fix(05): remove duplicate frontend tasks from 05-03, add requirements field
05-03 now backend-only (4 tasks/13 files): model+crypto+CRUD, providers, aggregation, prisma push. Frontend tasks moved exclusively to 05-04. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ plan: 03
|
|||||||
type: execute
|
type: execute
|
||||||
wave: 3
|
wave: 3
|
||||||
depends_on: ["05-01", "05-02"]
|
depends_on: ["05-01", "05-02"]
|
||||||
|
requirements: [CAL-01, CAL-02, CAL-03, DASH-05]
|
||||||
files_modified:
|
files_modified:
|
||||||
- apps/api/prisma/schema.prisma
|
- apps/api/prisma/schema.prisma
|
||||||
- apps/api/src/app.module.ts
|
- apps/api/src/app.module.ts
|
||||||
@@ -18,14 +19,6 @@ files_modified:
|
|||||||
- apps/api/src/calendar/dto/update-calendar-source.dto.ts
|
- apps/api/src/calendar/dto/update-calendar-source.dto.ts
|
||||||
- apps/api/src/calendar/dto/calendar-events-query.dto.ts
|
- apps/api/src/calendar/dto/calendar-events-query.dto.ts
|
||||||
- apps/api/package.json
|
- apps/api/package.json
|
||||||
- apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx
|
|
||||||
- apps/web/src/components/settings/calendar-source-form.tsx
|
|
||||||
- apps/web/src/components/settings/calendar-settings-panel.tsx
|
|
||||||
- apps/web/src/components/dashboard/widgets/calendar-widget.tsx
|
|
||||||
- apps/web/src/components/dashboard/widget-registry.ts
|
|
||||||
- apps/web/src/lib/calendar-api.ts
|
|
||||||
- apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx
|
|
||||||
- apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx
|
|
||||||
autonomous: true
|
autonomous: true
|
||||||
user_setup:
|
user_setup:
|
||||||
- service: calendar-encryption
|
- service: calendar-encryption
|
||||||
@@ -82,8 +75,6 @@ Symbols created by THIS plan (exclude from drift verification — they are new):
|
|||||||
**NestJS:** `CalendarModule`, `CalendarController`, `CalendarService`, `CalendarCryptoService`, `CalDAVProvider`, `ICSProvider`, `ExchangeProvider`, `CreateCalendarSourceDto`, `UpdateCalendarSourceDto`, `CalendarEventsQueryDto`
|
**NestJS:** `CalendarModule`, `CalendarController`, `CalendarService`, `CalendarCryptoService`, `CalDAVProvider`, `ICSProvider`, `ExchangeProvider`, `CreateCalendarSourceDto`, `UpdateCalendarSourceDto`, `CalendarEventsQueryDto`
|
||||||
**Interfaces:** `CalendarEvent`, `CalendarProvider`
|
**Interfaces:** `CalendarEvent`, `CalendarProvider`
|
||||||
**API endpoints:** `GET /api/calendar/sources`, `POST /api/calendar/sources`, `PATCH /api/calendar/sources/:id`, `DELETE /api/calendar/sources/:id`, `POST /api/calendar/sources/:id/test`, `GET /api/calendar/events`
|
**API endpoints:** `GET /api/calendar/sources`, `POST /api/calendar/sources`, `PATCH /api/calendar/sources/:id`, `DELETE /api/calendar/sources/:id`, `POST /api/calendar/sources/:id/test`, `GET /api/calendar/events`
|
||||||
**Frontend components:** `CalendarWidget`, `CalendarSettingsPanel`, `CalendarSourceForm`
|
|
||||||
**Frontend modules:** `calendar-api.ts` (fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents); registry wiring of real `CalendarWidget`
|
|
||||||
**npm:** `tsdav`, `node-ical`, `ews-javascript-api`, `@microsoft/microsoft-graph-client`
|
**npm:** `tsdav`, `node-ical`, `ews-javascript-api`, `@microsoft/microsoft-graph-client`
|
||||||
**env var:** `CALENDAR_ENCRYPTION_KEY`
|
**env var:** `CALENDAR_ENCRYPTION_KEY`
|
||||||
</artifacts_this_phase_produces>
|
</artifacts_this_phase_produces>
|
||||||
@@ -187,84 +178,33 @@ Symbols created by THIS plan (exclude from drift verification — they are new):
|
|||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 3: Event aggregation + caching + calendar widget</name>
|
<name>Task 3: Event aggregation + caching backend</name>
|
||||||
<files>apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts, apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/dashboard/widget-registry.ts, apps/web/src/lib/calendar-api.ts, apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx</files>
|
<files>apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
- apps/api/src/calendar/calendar.service.ts (from Task 1/2 — add aggregateEvents)
|
- apps/api/src/calendar/calendar.service.ts (from Task 1/2 — add aggregateEvents)
|
||||||
- apps/api/src/calendar/providers/*.provider.ts (from Task 2 — dispatch by type)
|
- apps/api/src/calendar/providers/*.provider.ts (from Task 2 — dispatch by type)
|
||||||
- apps/web/src/components/dashboard/widget-registry.ts (replace calendar placeholder with real CalendarWidget)
|
|
||||||
- apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps pattern)
|
|
||||||
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 408-413 (caching with TTL Pitfall 4)
|
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 408-413 (caching with TTL Pitfall 4)
|
||||||
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 110, 206-209 (calendar widget spec: time/title/source color dot; empty-state copy)
|
|
||||||
</read_first>
|
</read_first>
|
||||||
<behavior>
|
|
||||||
- Test (calendar-widget.test.tsx): given a mocked `/api/calendar/events` response with two events, the widget renders both titles, their times, and a source color dot per event; given an empty response renders t('widgets.calendarEmptyNoEvents'); given a no-sources response renders t('widgets.calendarEmptyNoSources')
|
|
||||||
</behavior>
|
|
||||||
<action>
|
<action>
|
||||||
Backend — add `aggregateEvents(userId, from, to)` to CalendarService: load the user's `isVisible: true` sources (CAL-02/CAL-03), dispatch each to its provider by `source.type` (ics/caldav/exchange), decrypt credentials per source, fetch in parallel (`Promise.allSettled` so one failing source doesn't break others — set lastSyncError on failures), merge + sort by start ascending, return normalized CalendarEvent[] including each event's source color. Cache results per user in an in-memory Map with a 5-minute TTL (Pitfall 4) — serve cached immediately, refresh in background. Implement `GET /calendar/events` in the controller using CalendarEventsQueryDto (default window: now → now+30 days). Implement `POST /calendar/sources/:id/test` → provider.testConnection, update lastSyncAt/lastSyncError.
|
Add `aggregateEvents(userId, from, to)` to CalendarService: load the user's `isVisible: true` sources (CAL-02/CAL-03), dispatch each to its provider by `source.type` (ics/caldav/exchange), decrypt credentials per source, fetch in parallel (`Promise.allSettled` so one failing source doesn't break others — set lastSyncError on failures), merge + sort by start ascending, return normalized CalendarEvent[] including each event's source color. Cache results per user in an in-memory Map with a 5-minute TTL (Pitfall 4) — serve cached immediately, refresh in background. Implement `GET /calendar/events` in the controller using CalendarEventsQueryDto (default window: now → now+30 days). Implement `POST /calendar/sources/:id/test` → provider.testConnection, update lastSyncAt/lastSyncError.
|
||||||
|
|
||||||
Frontend — `calendar-api.ts`: fetchSources, addSource, updateSource (incl. isVisible toggle), deleteSource, testSource, fetchEvents — all `credentials:'include'`.
|
|
||||||
|
|
||||||
`calendar-widget.tsx` (DASH-05, D-10 read-only): `'use client'`. On mount fetch /api/calendar/events. Render an upcoming-events list: each row shows time (locale-formatted), title, and an 8px source color dot (UI-SPEC color palette). Three states per UI-SPEC copy: no sources configured → t('widgets.calendarEmptyNoSources'); sources but no events → t('widgets.calendarEmptyNoEvents'); events → list. Loading state while fetching. Refresh periodically (e.g. every 5 min) to match backend cache. Never fetch external calendars directly from the browser (RESEARCH anti-pattern — always via /api/calendar/events).
|
|
||||||
|
|
||||||
Update `widget-registry.ts`: replace the calendar placeholder component with the real CalendarWidget. Keep WIDGET_CONSTRAINTS.
|
|
||||||
|
|
||||||
Write calendar-widget.test.tsx per <behavior> with mocked fetch.
|
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx && cd apps/api && npx tsc --noEmit</automated>
|
<automated>cd apps/api && npx tsc --noEmit</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- calendar.service.ts aggregateEvents uses `Promise.allSettled` and filters `isVisible`
|
- calendar.service.ts aggregateEvents uses `Promise.allSettled` and filters `isVisible`
|
||||||
- calendar.service.ts caches events with a TTL (grep: a Map + timestamp/expiry check)
|
- calendar.service.ts caches events with a TTL (grep: a Map + timestamp/expiry check)
|
||||||
- calendar.controller.ts contains `@Get('events')` and `@Post('sources/:id/test')`
|
- calendar.controller.ts contains `@Get('events')` and `@Post('sources/:id/test')`
|
||||||
- calendar-widget.tsx fetches `/api/calendar/events` and renders three distinct empty/list states
|
- `npx tsc --noEmit` exits 0
|
||||||
- widget-registry.ts references the real `CalendarWidget`
|
|
||||||
- calendar-widget.test.tsx exits 0; apps/api `tsc --noEmit` exits 0
|
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>Calendar widget shows aggregated upcoming events from visible sources, cached, read-only, with correct empty states.</done>
|
<done>Event aggregation across providers with caching, test-connection endpoint.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 4: Calendar settings page (source management + visibility)</name>
|
<name>Task 4: [BLOCKING] Prisma schema push + encryption key check</name>
|
||||||
<files>apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/settings/calendar-source-form.tsx, apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx</files>
|
|
||||||
<read_first>
|
|
||||||
- apps/web/src/app/(portal)/settings/layout.tsx (from 05-01 — nested settings layout)
|
|
||||||
- apps/web/src/components/settings/settings-sidebar.tsx (from 05-01 — this page is the "Kalender" sub-item target)
|
|
||||||
- apps/web/src/lib/calendar-api.ts (from 05-03 Task 3)
|
|
||||||
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 120, 161-166, 204-209, 216-220, 256-261 (calendar settings panel spec, source form fields, connection test, delete confirmation, copy)
|
|
||||||
</read_first>
|
|
||||||
<behavior>
|
|
||||||
- Test (calendar-settings.test.tsx): rendering CalendarSettingsPanel with two mocked sources lists both with name + type badge + visibility toggle; toggling a source calls updateSource with the new isVisible; the add form requires name+type+url before enabling submit
|
|
||||||
</behavior>
|
|
||||||
<action>
|
|
||||||
Create `settings/dashboard/calendar/page.tsx` (D-11): `'use client'`, renders CalendarSettingsPanel. Title t('settings.categoryCalendar').
|
|
||||||
|
|
||||||
Create `calendar-settings-panel.tsx`: fetch sources (calendar-api.fetchSources). Render a source list — each row: color dot, name, type badge (CalDAV/Exchange/ICS), visibility toggle switch (CAL-02 — calls updateSource({isVisible})), connection-status indicator (green check / orange warning from lastSyncError per UI-SPEC), edit + delete actions. Delete uses a confirmation dialog (UI-SPEC destructive: heading + body + "Quelle loeschen"/"Abbrechen"). "Quelle hinzufuegen" button reveals CalendarSourceForm. Empty state: t('settings.calendarEmpty') (UI-SPEC copy).
|
|
||||||
|
|
||||||
Create `calendar-source-form.tsx`: fields Name (required), Type (select CalDAV/Exchange/ICS, required), when Exchange show an Exchange-mode select (Exchange Online=graph / Exchange Server=ews, RESEARCH open question 2), URL (required, https validation client-side), Username (optional, hidden for ICS), Password (password input, optional, hidden for ICS), Color (from the 8-color palette in UI-SPEC). On save call addSource (or updateSource when editing); then auto-run testSource and show connection-success/error toast (UI-SPEC copy). Validate URL is https before submit.
|
|
||||||
|
|
||||||
Write calendar-settings.test.tsx per <behavior> with mocked calendar-api.
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>cd apps/web && pnpm vitest run "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx" && pnpm exec tsc --noEmit</automated>
|
|
||||||
</verify>
|
|
||||||
<acceptance_criteria>
|
|
||||||
- settings/dashboard/calendar/page.tsx renders CalendarSettingsPanel
|
|
||||||
- calendar-settings-panel.tsx has a visibility toggle calling updateSource with isVisible
|
|
||||||
- calendar-source-form.tsx shows an Exchange-mode select only when type is exchange, and hides username/password for ICS
|
|
||||||
- calendar-source-form.tsx validates https before submit
|
|
||||||
- delete uses a confirmation dialog (contains the "Quelle loeschen"/"Delete source" CTA)
|
|
||||||
- calendar-settings.test.tsx exits 0; `pnpm exec tsc --noEmit` exits 0
|
|
||||||
</acceptance_criteria>
|
|
||||||
<done>Users manage CalDAV/Exchange/ICS sources, toggle widget visibility, test connections, and delete with confirmation.</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="auto">
|
|
||||||
<name>Task 5: [BLOCKING] Prisma schema push + encryption key check</name>
|
|
||||||
<files>apps/api/prisma/schema.prisma</files>
|
<files>apps/api/prisma/schema.prisma</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
- apps/api/prisma/schema.prisma (CalendarSource model from Task 1 must exist)
|
- apps/api/prisma/schema.prisma (CalendarSource model from Task 1)
|
||||||
- apps/api/src/calendar/crypto.service.ts (CALENDAR_ENCRYPTION_KEY consumer)
|
- apps/api/src/calendar/crypto.service.ts (CALENDAR_ENCRYPTION_KEY consumer)
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
|
|||||||
Reference in New Issue
Block a user