docs: specify calendar type for external integrations (B.19) - #2807
docs: specify calendar type for external integrations (B.19)#2807Pierre-Gilles wants to merge 13 commits into
Conversation
Design for community request (forum topic 10432): open calendar providers (Google Calendar, Outlook, Nextcloud/Framagenda, public ICS feeds) as external integrations, feeding the same calendar view and scene triggers as the internal CalDAV service. - New section B.19: push model into t_calendar/t_calendar_event, per-user accounts via an account_schema (the B.15 machinery), host API contract (account listing, calendar/event upsert by external_id, window prune), normalization bounds, sync semantics (field ownership, sync/shared toggles, uninstall cleanup), and a per-user OAuth2 milestone-2 sketch for Google/Microsoft. - Core prerequisites called out: calendar upsert primitives, buildUniqueSelector for calendar/event selectors, ownership checks on the user calendar routes, explicit uninstall cleanup, calendar WebSocket refresh, CALENDAR_TYPES constant, double-associate fix, frontend color-join guard. - Contracts updated: manifest type enum (C.1), WS protocol row (C.4), non-admin reduced view (C.5), SDK methods and handler (C.8), test plan (B.13), phase table and spec header. - AGENTS.md: refresh the external-integrations spec pointer (B.15-B.18 shipped, B.19 specified but not implemented). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe pull request implements shipped calendar integrations. It adds per-user accounts, calendar and event synchronization, ownership checks, APIs, WebSocket updates, frontend management, manifest support, uninstall cleanup, and tests. ChangesCalendar integration implementation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CalendarConfig
participant ExternalIntegration
participant Calendar
participant WebSocket
User->>CalendarConfig: Save account or change calendar settings
CalendarConfig->>ExternalIntegration: Submit account or calendar update
ExternalIntegration->>Calendar: Persist account, calendars, or events
Calendar-->>ExternalIntegration: Return updated calendar state
ExternalIntegration->>WebSocket: Send calendar.updated or account-updated
WebSocket-->>User: Refresh calendar data
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🐳 A Docker image has been built for this branch and pushed to the GitHub Container Registry. You can test this pull request (AMD64 only) by pulling the image below: For example, run it with: sudo docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--cgroupns=host \
--restart=always \
--privileged \
--network=host \
--name gladys-claude-calendar-integrations-spec-y64vwe \
-e NODE_ENV=production \
-e SERVER_PORT=80 \
-e TZ=Europe/Paris \
-e SQLITE_FILE_PATH=/var/lib/gladysassistant/gladys-production.db \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/gladysassistant:/var/lib/gladysassistant \
-v /dev:/dev \
-v /run/udev:/run/udev:ro \
ghcr.io/gladysassistant/gladys-preview:claude-calendar-integrations-spec-y64vweThis comment and the image are automatically updated on every new commit pushed to this pull request. Need an ARM64 image (Raspberry Pi, Apple Silicon, …)? Comment |
There was a problem hiding this comment.
Stale comment
Spec-only PR for the calendar external-integration type (B.19). Overall direction looks sound: push model (integration syncs, core stores), per-user consent,
sharedas the scene-automation boundary, and reuse of B.15/B.18 patterns. Code claims I spot-checked (shared-only scene triggers, missing ownership on calendar update/destroy, doublecalendar.associate, color join TypeError, CalDAV as sole producer) match the current tree.Not ready to approve yet — the living-spec contracts are incomplete or inconsistent in a few places that would mislead an implementer:
- C.5 table drift — the prose now includes calendar for non-admins, but the route table still says communication-only (list + detail), and
DELETE .../:selectorstill omits calendars from the uninstall contract that B.19 says must be explicit.- Multi-user
external_idcollision —t_calendar.external_idandt_calendar_event.external_idare globally UNIQUE; the example and conventions do not embed the Gladys user, so two household members enabling the same provider (or the same public ICS feed) will collide / steal rows.- Window prune vs multi-day events — start-only window membership + "start must be in window" rejects updates for events that start before the rolling window.
- Enable-without-
account_schemaand B.2 uninstall wording need tightening so the implementation checklist is unambiguous.Also worth aligning C.1 (
account_schemanext tocontact_schemafor{{port:<name>}}refusal andsourcevalidation) even though those lines are outside this diff's hunks.No
server/utils/constants.jsdevice categories/types in this PR. Docs-only → notrisk:high. Flaggingneeds:human-reviewfor philosophy/trust-boundary alignment (unaudited container → calendars → shared → scene triggers), withatrovatorequested.Sent by Cursor Automation: Automatic PR review
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2807 +/- ##
==========================================
+ Coverage 99.49% 99.50% +0.01%
==========================================
Files 1221 1233 +12
Lines 85594 86974 +1380
==========================================
+ Hits 85158 86543 +1385
+ Misses 436 431 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@AGENTS.md`:
- Line 30: Update the phase-2 workstream count in the external integrations
guidance from “three” to “four,” leaving the listed workstreams and remaining
wording unchanged.
In `@docs/specs/external-integrations.md`:
- Line 273: Update the verification section around the documented test command
to use the CI-required coverage command, `npm run coverage`, instead of `cd
server && npm test`. Keep the surrounding verification requirements unchanged.
- Line 423: Add `calendar.updated` to the C.4 frontend WebSocket message list,
including its payload and recipient rule: send it to the owning user, or to
everyone when the calendar is shared. Keep the existing real-time refresh
behavior and contract details consistent with the B.19 entry.
- Line 470: The normalization contract around external_id must support moving an
event between calendars without losing its existing row. Define a stable
provider event identity independent of calendar_id, or specify an explicit move
key that lets upsert locate and update the prior row while preserving the
required per-integration uniqueness and calendar-embedded external_id behavior.
- Line 812: Update both C.5 non-admin route rows to allow calendar integrations
in addition to communication integrations, while preserving the existing
reduced-view response and 404 behavior for unsupported selectors such as type
"device". Ensure the route table aligns with the B.19 per-user calendar-account
flow and the surrounding access rules.
- Line 433: Update the Host API external-ID convention to include a stable
per-user account namespace in both t_calendar.external_id and
t_calendar_event.external_id, or explicitly define account-scoped identity and
upsert keys that prevent cross-user collisions. Ensure the documented publish
and update behavior uses this account scope while preserving the existing
ext:<selector>: convention.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: db5c8d25-0bbb-470a-ae9e-742e2fe67b17
📒 Files selected for processing (2)
AGENTS.mddocs/specs/external-integrations.md
The C.5 prose extended the non-admin reduced view to calendar integrations, but the route table still said communication-only and the uninstall row omitted calendars. Bring the table in line: list and detail rows include calendar-type integrations for non-admins, and DELETE .../:selector explicitly removes the service's calendars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
Deploying gladys-plus with
|
| Latest commit: |
16c09d6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f334be0e.gladys-plus.pages.dev |
| Branch Preview URL: | https://claude-calendar-integrations.gladys-plus.pages.dev |
There was a problem hiding this comment.
Stale comment
Re-review at
9c39878(after the C.5 table alignment commit).Progress since last review: the C.5 route table and
DELETE .../:selectoruninstall wording now match the B.19 prose (non-admin list/detail include calendar; uninstall lists calendars). That prior blocker is cleared.Still not ready to approve — several living-spec contracts remain incomplete or internally inconsistent, and would mislead an implementer:
- Multi-user
external_iduniqueness — global UNIQUE columns + example ids that omit the Gladys user → household collisions / possible row steal under the "move" rule.- Event move vs calendar-embedded
external_id— point 4 contradicts itself (embed calendar in the id, then upsert-by-external_idto move).- Window prune is start-only — multi-day / straddling events under rolling windows are underspecified.
- Enable without
account_schema— public-feed Enable gesture still has no empty-schema request contract.- B.2 uninstall — B.19 claims the B.2 contract was updated; section B.2 still only lists devices/variables/containers (C.5 DELETE was fixed; B.2 was not).
calendar.updatedmissing from the C.4 frontend WS list — defined in B.19 but omitted from the Core → frontend contract paragraph.- Soft: phrase
hasDeviceScreensas a required front change (today onlycommunication/weather); extend the C.4 reconnect bullet (GET /device+GET /config) to the calendar surface.Code claims re-checked and still match the tree (
shared: truescene filter, global UNIQUEexternal_id, doublecalendar.associate, color join TypeError, update/destroy by selector alone,hasDeviceScreensexcludes only communication/weather).No
server/utils/constants.jsdevice categories/types. Docs-only → notrisk:high. Keepingneeds:human-review+atrovatofor the trust-boundary / philosophy call (unaudited container → calendars →shared→ scene triggers).Sent by Cursor Automation: Automatic PR review
- User-scoped external ids: calendar and event external_ids must carry
the ext:<selector>:<user_selector>: prefix, enforced server-side, so
two household members syncing the same provider-side id can never
collide on the globally UNIQUE columns (example updated).
- Resolve the event-move contradiction: the event external_id is a
stable per-user identity independent of the calendar; moves update
calendar_id, and calendar-embedded ids explicitly renounce moves.
- Window prune membership is by overlap (start < to and (end ?? start)
>= from), so multi-day events straddling a rolling window stay
updatable and prunable; pushed events must overlap the window.
- Explicit empty-schema contract for the per-user Enable gesture
(config {} or omitted, any key -> 422) and the reserved
EXTERNAL_INTEGRATION_CALENDAR_ACCOUNT enablement variable.
- B.2 uninstall contract now lists calendars; Verification uses
npm run coverage (the CI command).
- calendar.updated added to the C.4 core->frontend WS contract, and
the reconnection resync contract extended to the calendar surface.
- C.1 aligned: account_schema next to contact_schema for the
{{port:<name>}} refusal and dynamic source validation; required
front changes (EXTERNAL_CATEGORIES, hasDeviceScreens) spelled out;
B.13 tests updated accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
There was a problem hiding this comment.
Stale comment
Re-review at
76a6b98(address-review-feedback commit).Prior blockers are cleared. User-scoped
ext:<selector>:<user_selector>:ids, calendar-independent event identity (with explicit renounce-moves path), overlap-based window prune, empty-account_schemaEnable contract +EXTERNAL_INTEGRATION_CALENDAR_ACCOUNT, B.2 uninstall calendars wording,calendar.updatedin the C.4 frontend WS list, C.4 reconnect resync, C.1account_schemaalignment, and required front changes (EXTERNAL_CATEGORIES/hasDeviceScreens) are all in the living spec now. Code claims re-checked and still match the tree.Approving the specification. Soft residuals only (inline) — not merge blockers for a docs-only PR:
- Frontend exclusion lists also live outside
ExternalIntegrationPage(install / discover / device pages).- Exclusive-
endday-boundary overlap formula can over-include full-day events at window edges.No
server/utils/constants.jsdevice categories/types. Docs-only → notrisk:high. Keepingneeds:human-review+atrovatofor the trust-boundary / philosophy call (unaudited container → calendars →shared→ scene triggers).needs:cursor-reviewremoved.Sent by Cursor Automation: Automatic PR review
- Renumber the spec section B.19 -> B.20: open PR #2807 already reserves B.19 for the calendar provider type. - Never sweep a freshly pulled image. `install` and `update` pull their images *before* writing the `t_service` row that declares them; a sweep landing in that window saw a brand new image as an orphan and deleted it under the operation that had just fetched it — and Docker's 409 is no help there, the container does not exist yet. `system.pull` now stamps every pull (before the download, so a slow pull on a Raspberry Pi is covered end to end) and the sweep skips anything pulled less than an hour ago. - Pin `force: false` in `system.removeImage`: the "never force" rule is the safety invariant of every caller, so it belongs in the helper rather than in call-site discipline. - Align the update comment and the spec with what `start()` actually guarantees: it resolves on LOADING, not RUNNING, so the cleanup buys the container being created and started, never a healthy integration. The ordering still matters — a `start()` that throws skips the cleanup entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zFg1ZJerC7KcgxrkApuri
Keep both the B.19 calendar-type section (this branch) and the B.20 Docker image cleanup section added on master at the same insertion point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/specs/external-integrations.md`:
- Line 275: Expand the B.13 `calendar.updated` invalidation coverage to define
notifications for state transitions: notify the union of old and new viewers
when `shared` changes, include both old and new calendar selectors when events
move, and notify previous viewers for account disable and uninstall deletions.
Add these affected-calendar and affected-viewer cases to the B.13 coverage list
and reflect the same complete invalidation set in the related calendar
specification entries.
- Around line 430-431: Update the canonical manifest table in the
manifest-contract section to define optional account_schema for calendar
integrations, including the same flat-schema validation and rendering
restrictions as config_schema/contact_schema. Document milestone-1 rejection of
oauth2 fields and {{port:<name>}}, while preserving the existing config_schema
behavior and referencing the established B.19 calendar semantics.
- Line 604: Update dynamic "devices" option resolution and value validation for
per-user schemas such as account_schema and contact_schema to filter by both the
integration's t_service and the requesting user. Preserve integration-wide
device scoping for non-user-specific schemas, while ensuring per-user forms
never expose or accept another user's device identifier.
- Line 472: The normalization specification’s recurrence identity must
distinguish each materialized occurrence instead of using the provider UID
alone. Update the recurrence guidance around external_id to require a
deterministic occurrence-specific suffix or key, such as the occurrence date,
while retaining the user-scoped prefix and independence from calendar_id so
moving an occurrence between calendars still updates the existing row.
- Line 435: The Host API specification requires a calendar-specific way to
construct user-scoped external IDs, but the existing externalId contract only
covers selector-scoped IDs. Update the SDK contract and the related C.7 examples
to define a calendar-aware helper that produces
ext:<selector>:<user_selector>:... IDs, or document and validate the composed
suffix explicitly in calendar methods; ensure the same requirement is covered at
the additionally referenced section.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4750a72e-d2b4-4588-a4b3-6fca494dcfc4
📒 Files selected for processing (2)
AGENTS.mddocs/specs/external-integrations.md
🚧 Files skipped from review as they are similar to previous changes (1)
- AGENTS.md
- CALENDAR_TYPES constant (CALDAV, WEBCAL, EXTERNAL) replacing the free strings in the CalDAV service; WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATED and EXTERNAL_INTEGRATION.CALENDAR_ACCOUNT_UPDATED message types. - calendar.upsertCalendars / calendar.upsertEvents: transactional batch upserts keyed by external_id, buildUniqueSelector for collision-free selectors, integration-owned vs user-owned field split, cross-owner ConflictError, overlap-based window prune sparing manual events, and the 10000-events-per-calendar cap. - Ownership checks on the user calendar routes: update/destroy and the event write routes now answer 404 on another user's calendar. - Explicit calendar cleanup in externalIntegration.uninstall (no silent FK cascade), fix of the double calendar.associate definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
There was a problem hiding this comment.
Stale comment
Re-review at
eec9cbb(master merge into the branch; prior approval was dismissed).B.19 content is unchanged from the previously approved
76a6b98. The merge only brought already-shipped living-spec updates (notably B.20 image cleanup) into the same file. Prior blockers remain cleared: user-scopedext:<selector>:<user_selector>:ids, calendar-independent event identity, overlap window prune, empty-account_schemaEnable +EXTERNAL_INTEGRATION_CALENDAR_ACCOUNT, B.2/C.5 uninstall calendars,calendar.updatedin C.4, reconnect resync, C.1account_schemaalignment, and required front changes.Code claims re-checked against the tree: scene triggers/actions require
shared: true;EXTERNAL_CATEGORIES/hasDeviceScreensstill omit calendar; doublecalendar.associate; global UNIQUE on calendar/eventexternal_id.Approving again. Soft residuals only (not merge blockers for a docs-only PR):
- Frontend exclusion surface also includes install/discover/device pages.
- Exclusive-
endday-boundary overlap can over-include full-day events at window edges.calendar.updatedrecipient rule should cover state transitions (sharedtrue→false, event moves across calendars, disable/uninstall) so previous viewers invalidate too — fine to nail down at implementation time if preferred.CodeRabbit’s other new notes are non-blocking here:
account_schemafollows the same C.1 omission pattern ascontact_schema(documented in B.19/B.15); user-scoped ids compose viaexternalId("${user}:${suffix}"); recurrence already cites the CalDAVUID + occurrence dateprecedent; per-usersource: "devices"scoping is not a calendar-account concern.No
server/utils/constants.jsdevice categories/types. Docs-only → notrisk:high. Keepingneeds:human-review+atrovatofor the trust-boundary / philosophy call (unaudited container → calendars →shared→ scene triggers). Removingneeds:cursor-review.Sent by Cursor Automation: Automatic PR review
…ts, management routes (B.19)
- Manifest: calendar type in the enum, account_schema (same flat format
and engine as contact_schema; oauth2 and {{port}} refused), schema and
validateManifest coverage.
- Host API /api/integration/v1/calendar*: account listing (secrets
included), calendar listing with sync flags, calendar upsert (user-scoped
external_id prefix enforced, bounds, invalid color dropped, 50/user cap),
calendar deletion, event batch upsert (bounds, overlap window prune,
sync-disabled 403), 30 writes/min rate limit, type gate 403, tenant
isolation.
- Per-user management routes: GET/POST/DELETE .../calendar/account and
PATCH .../calendar/:calendar_selector (sync/shared only; sync off
empties events), enablement variable EXTERNAL_INTEGRATION_CALENDAR_ACCOUNT
(enabled-with-zero-fields is first-class), non-admin reduced view
extended to calendar integrations.
- Real-time: calendar.updated pushed to the owner (all users when shared),
calendar.account-updated fire-and-forget to the integration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
…lock, live calendar view (B.19) - Catalog: EXTERNAL_CATEGORIES and the card type mapping gain 'calendar', non-admins see installed calendar integrations (reduced view), their cards land on the Configuration screen like communication/weather. - Generic page: hasDeviceScreens excludes the calendar type; new per-user 'My calendars' card (account_schema fields via ConfigField, Enable/Save, per-calendar sync/shared toggles with optimistic rollback, inline-confirmed disable destroying the user's calendars). - Calendar view: listens to the calendar.updated WebSocket message and refetches the displayed range; the color join no longer crashes on a shared calendar absent from the user's list. - i18n: integration.externalIntegration.myCalendars block in en/fr/de. - Spec kept truthful in the same diff: B.19 milestone 1 marked shipped (per-user OAuth2 stays milestone 2, design-only), AGENTS.md pointer refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
Seeding 9999 rows exceeds the default 2s mocha timeout under the parallel full-suite load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
There was a problem hiding this comment.
Stale comment
Re-review at
da71036— this is no longer a docs-only PR. Since the last approval (eec9cbb), milestone-1 B.19 has been implemented (core upserts, host/management APIs, front My calendars + live view). Prior living-spec contracts look largely followed (user-scopedext:<selector>:<user_selector>:ids, ownership on user calendar write routes, overlap prune, empty-schema Enable, rate limits, tenant isolation,sharedas scene boundary).Not ready to approve — concrete issues plus red CI:
- 10k event cap bypassed on moves in
calendar.upsertEvents(create path checks the cap; move path incrementscountwithout checking).sharedtrue→false under-notifies — Sequelize mutatescalendar.sharedbefore notify, so household clients keep seeing now-private events (updateUserCalendar).- Event moves / uninstall do not complete the
calendar.updatedinvalidation set (destination-only notify; uninstall destroys with no push). Disable is correct.- Front exclusion incomplete —
ExternalIntegrationPage.hasDeviceScreensincludescalendar, but install / discover / device pages still special-case only['communication','weather']. Empty-config_schemacalendar installs land on Devices; deep links keep rendering device UIs. (Those lines are outside prettier hunks, so called out here rather than inline.)- CI red: Server lint
prettier-checkfails on several new calendar files; Front testprettier-checkfails across a large trailing-comma sweep (~630 front files) that also left calendar-touched files unformatted. Please runnpm run prettierinserver/andfront/(or drop the unrelated mass reformat and format only the intentional touch set).Soft / non-blocking:
- Exclusive-
endoverlap still uses(end ?? start) ≥ from(matches current B.19 text; full-day events ending exactly atfromcan over-include).updateEventnow returns nestedcalendar: { user_id }via the ownershipinclude— possible API shape regression for clients ofPATCH /calendar/event/:selector.- PR title/description still say “docs / specification only”; worth updating now that milestone 1 has shipped in-tree.
No new
DEVICE_FEATURE_CATEGORIES/TYPES. Not taggingrisk:high(additive external-integration surface, no migration). Keepingneeds:human-review+atrovatofor the trust-boundary / philosophy call (unaudited container → calendars →shared→ scene triggers). Removingneeds:cursor-review.Sent by Cursor Automation: Automatic PR review
Earlier commits were formatted with a prettier v3 fetched by npx while node_modules was still installing: the pinned prettier 1.19 (the CI version) flagged both workspaces. Re-run npm run prettier everywhere with the project version — formatting only, no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
…den the window semantics Review feedback on the B.19 implementation: - The 10000-events-per-calendar cap now also holds on the move path: a destination calendar at the cap refuses an incoming move with a 400. - Unsharing a calendar (shared true->false) still pushes calendar.updated to everyone one last time, so previous household viewers drop it (snapshot before the Sequelize update mutates the instance). - An event move pushes the union of the source and destination calendar selectors (upsertEvents reports movedFromCalendarIds), everyone when either is shared. - Uninstall notifies the previous viewers of the destroyed calendars, grouped per owning user. - Window overlap is exclusive on the end side (start < to, and end > from when end is set, else start >= from): a full-day event ending exactly at belongs to the previous window, both for the prune and for the pushed-events check. - Front: the calendar type joins the communication/weather exclusion sets of install-page (post-install redirect) and the discover/device page guards, not only the page chrome. - Spec updated in the same diff (overlap formula, transition-push union rule, B.13 coverage, front call sites). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/lib/calendar/calendar.updateEvent.js (1)
33-33: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRestrict the update payload to editable event fields.
The route passes
req.bodyunchanged, andcalendar_idis a writable model attribute. The ownership check runs before the update, so a user can reassign an event to another calendar, including one owned by another user. Add a regression test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/lib/calendar/calendar.updateEvent.js` at line 33, Restrict the payload passed to existingCalendarEvent.update in the event update flow to approved editable event fields, excluding calendar_id and other ownership-sensitive attributes. Preserve the existing ownership validation and add a regression test confirming a user cannot reassign an event to another calendar.
🧹 Nitpick comments (10)
server/test/lib/external-integration/externalIntegration.uninstall.test.js (1)
55-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATEDinstead of the string literal.Line 58 compares the message type against
'calendar.updated'. The sibling testserver/test/lib/external-integration/externalIntegration.calendar.test.jsuses the constant. If the constant value changes, the filter here silently returns an empty array and the test fails with a confusing count mismatch.♻️ Proposed change
- .filter((call) => call.args[1].type === 'calendar.updated'); + .filter((call) => call.args[1].type === WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATED);Add the import at the top of the file:
const { WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/external-integration/externalIntegration.uninstall.test.js` around lines 55 - 62, Import WEBSOCKET_MESSAGE_TYPES from the established constants module, then update the calendarPushes filter to compare against WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATED instead of the hardcoded 'calendar.updated' string.server/test/lib/external-integration/externalIntegration.calendar.test.js (2)
218-223: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert that a matching
websocket.sendcall exists before you read the last one.If the filters at Line 220 and Line 221 return an empty array, Line 222 evaluates
sendCalls[-1].argsand throws aTypeError. The failure then hides the real cause. The sibling tests at Line 197 and Line 232 guard with a length assertion first. Apply the same guard here.🛡️ Proposed guard
const sendCalls = event.emit .getCalls() .filter((call) => call.args[0] === 'websocket.send') .filter((call) => call.args[1].type === WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATED); + expect(sendCalls.length).to.be.greaterThan(0); const lastPayload = sendCalls[sendCalls.length - 1].args[1].payload;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/external-integration/externalIntegration.calendar.test.js` around lines 218 - 223, In the test around the sendCalls filtering, assert that sendCalls contains a matching call before accessing sendCalls[sendCalls.length - 1].args. Mirror the existing length guard used by the sibling tests, then retain the lastPayload and calendar_selectors assertions.
193-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider exact counts instead of lower bounds.
Line 197 asserts
greaterThan(1)and Line 232 assertsgreaterThan(0). Both tests perform a known number of updates, so the expected number ofwebsocket.send-allpushes is deterministic. An exact count detects a regression that emits duplicate pushes. A lower bound does not.Also applies to: 228-232
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/external-integration/externalIntegration.calendar.test.js` around lines 193 - 197, Update the assertions for filtered CALENDAR.UPDATED websocket.send-all calls in the affected tests to use the deterministic expected counts instead of greaterThan lower bounds. Adjust both assertions near the first update scenario and the later scenario so duplicate pushes cause failure, while preserving the existing event filtering.server/test/lib/calendar/calendar.upsertCalendars.test.js (1)
55-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStrengthen the rollback assertion with a real partial batch.
The array at Line 57 holds one calendar. The conflict throws before any write, so Line 62 cannot detect a rollback failure. To exercise the transaction, send a new calendar first and the conflicting one second, then assert that the new calendar was not persisted.
♻️ Proposed stronger case
it('should refuse to steal a calendar of another owner', async () => { await calendar.upsertCalendars(USER_A, SERVICE_ID, [{ external_id: 'ext:my-int:shared-id', name: 'Mine' }]); const promise = calendar.upsertCalendars(USER_B, SERVICE_ID, [ + { external_id: 'ext:my-int:pepper:fresh', name: 'Fresh' }, { external_id: 'ext:my-int:shared-id', name: 'Stolen' }, ]); await assert.isRejected(promise, 'already belongs to another owner'); // and nothing was partially applied const row = await db.Calendar.findOne({ where: { external_id: 'ext:my-int:shared-id' } }); expect(row.name).to.equal('Mine'); + const rolledBack = await db.Calendar.findOne({ where: { external_id: 'ext:my-int:pepper:fresh' } }); + expect(rolledBack).to.equal(null); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/calendar/calendar.upsertCalendars.test.js` around lines 55 - 64, Update the test case around calendar.upsertCalendars to submit a new calendar before the conflicting calendar owned by USER_A, ensuring the conflict occurs after an attempted write. Assert that the existing calendar remains unchanged and the new calendar is absent from the database, verifying the whole batch was rolled back.server/test/lib/external-integration/testUtils.test.js (1)
108-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe calendar manifest fixture is defined three times.
testUtils.test.jsexportsTEST_CALENDAR_MANIFEST, but both controller test files declare their own near-identicalCALENDAR_MANIFESTinstead of importing it. The three copies will drift as the manifest contract evolves.
server/test/lib/external-integration/testUtils.test.js#L108-L123: keep this fixture as the single source, and confirm it stays importable from the controller test directory.server/test/controllers/externalIntegration/externalIntegration.calendar.test.js#L7-L19: replace the localCALENDAR_MANIFESTwith an import ofTEST_CALENDAR_MANIFEST, and apply the per-test overrides through a spread.server/test/controllers/integrationHost/integrationHost.calendar.test.js#L9-L18: replace the localCALENDAR_MANIFESTwith the same import; this copy also drops theapp_passwordsecret field, which is a real divergence from the shared fixture.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/external-integration/testUtils.test.js` around lines 108 - 123, The calendar manifest fixture is duplicated across three test files; keep TEST_CALENDAR_MANIFEST in server/test/lib/external-integration/testUtils.test.js:108-123 as the single source and ensure it remains importable from the controller tests. In server/test/controllers/externalIntegration/externalIntegration.calendar.test.js:7-19, replace CALENDAR_MANIFEST with the shared import and apply per-test changes via object spreading; make the same replacement in server/test/controllers/integrationHost/integrationHost.calendar.test.js:9-18, preserving the shared app_password field.server/test/controllers/integrationHost/integrationHost.calendar.test.js (2)
142-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClear the timers of every seeded service, not only in the happy path.
Line 242 calls
clearTimers(otherService.id)at the end of the test body. ThedeviceServiceseeded at Line 143 never gets aclearTimerscall. If an assertion throws before Line 242, the timers ofotherServicealso stay armed. Track the seeded service ids and clear them in the existingafterEach.♻️ Proposed cleanup tracking
describe('Integration host API: calendar', () => { let gladys; let service; let token; + let seededServiceIds; const prefix = 'ext:ext-dev-nextcloud-calendar:john:'; beforeEach(async () => { gladys = global.TEST_GLADYS_INSTANCE; service = await seedService(); + seededServiceIds = [service.id]; token = generateIntegrationToken(service.id, 1, 'secret'); await gladys.externalIntegration.saveCalendarAccount(service.selector, JOHN_USER_ID, { server_url: 'https://cloud.example.com', }); }); afterEach(() => { gladys.externalIntegration.calendarWriteRateLimits.clear(); - gladys.externalIntegration.clearTimers(service.id); + seededServiceIds.forEach((id) => gladys.externalIntegration.clearTimers(id)); });Then push each newly seeded service id into
seededServiceIdsand remove the inline call at Line 242.Also applies to: 219-243
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/controllers/integrationHost/integrationHost.calendar.test.js` around lines 142 - 166, Track every service created by these tests in the existing cleanup mechanism, including deviceService and otherService, by adding each seeded service ID to seededServiceIds. Update the existing afterEach to clearTimers for all tracked IDs, and remove the inline clearTimers(otherService.id) call so cleanup also runs when assertions fail.
245-259: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winControl the clock in this rate-limit test.
calendarWriteRateLimitsstarts a 60-second window at the first request. If the 30 sequential requests take longer than 60 seconds, the counter resets and the 31st request returns200. StubDate.nowor use a fake clock so all requests use one window.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/controllers/integrationHost/integrationHost.calendar.test.js` around lines 245 - 259, Update the rate-limit test around “should rate limit the write endpoints to 30 per minute” to control time with a stubbed Date.now or fake clock, keeping all 31 requests within the same 60-second window. Restore the clock after the test so other integration tests retain normal time behavior.server/test/controllers/externalIntegration/externalIntegration.calendar.test.js (1)
49-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an
afterEachto clear calendar rate limits and timers.Clear
calendarWriteRateLimitsand callgladys.externalIntegration.clearTimers()after each test so failed tests cannot leak shared state or pending timers into later tests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/controllers/externalIntegration/externalIntegration.calendar.test.js` around lines 49 - 53, Add an afterEach hook in the “External integration management API: my calendars” suite to clear calendarWriteRateLimits and call gladys.externalIntegration.clearTimers() after every test, preventing shared rate-limit state and pending timers from leaking between tests.front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx (1)
26-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the "no device screens" manifest-type list into one shared constant. Four frontend sites now repeat
['communication', 'weather', 'calendar']. A future type must be added to every copy, and a missed copy produces an inconsistent route. Export one constant (for example fromfront/src/routes/integration/all/external-integration/utils) and import it at each site.
front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx#L26-L32: replace the inline array in thehasDeviceScreenscomputation with the shared constant.front/src/routes/integration/all/external-integration/device-page/index.js#L16-L18: replace the inline array in the redirect condition with the shared constant.front/src/routes/integration/all/external-integration/discover-page/index.js#L44-L46: replace the inline array in the redirect condition with the shared constant.front/src/routes/integration/index.js#L227-L230: replace the inline array ingetInstalledUrl, and reuse it for the cardtypemapping at lines 244 and 266.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx` around lines 26 - 32, Extract the shared no-device-screen manifest-type list into an exported constant, then import and reuse it at front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx lines 26-32, device-page/index.js lines 16-18, discover-page/index.js lines 44-46, and front/src/routes/integration/index.js lines 227-230. Replace each inline array, and reuse the constant for the card type mappings at lines 244 and 266 in front/src/routes/integration/index.js.server/lib/calendar/calendar.upsertEvents.js (1)
6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the event cap to the shared constants module.
MAX_EVENTS_PER_CALENDARis declared locally, while the related calendar limits live inserver/lib/external-integration/constants.js(MAX_CALENDARS_PER_USER,MAX_CALENDAR_EVENTS_PER_REQUEST). A single location keeps the limits discoverable and testable. This is optional and can be deferred.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/lib/calendar/calendar.upsertEvents.js` at line 6, Move MAX_EVENTS_PER_CALENDAR from calendar.upsertEvents.js into the shared external-integration constants module alongside MAX_CALENDARS_PER_USER and MAX_CALENDAR_EVENTS_PER_REQUEST, then update the event upsert code to import and use the shared constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@front/src/routes/integration/all/external-integration/config-page/index.js`:
- Around line 326-347: Update toggleUserCalendar to use functional setState
updaters for both the optimistic change and failure rollback, rather than
reading this.state or capturing a full calendarAccount snapshot. Preserve
concurrent calendar toggles by deriving each update from the latest previous
state, and on failure restore only the affected calendar’s original value while
retaining other toggles.
In `@server/api/controllers/integrationHost.controller.js`:
- Around line 245-259: Validate req.query.user in getIntegrationCalendars before
calling gladys.externalIntegration.getIntegrationCalendars: when present, accept
only a string and reject arrays or objects by raising BadParameters so the
endpoint returns HTTP 400. Preserve the existing behavior for an omitted user,
and add coverage for repeated and bracketed user query parameters.
In `@server/lib/calendar/calendar.updateEvent.js`:
- Around line 20-29: Update the existingCalendarEvent validation in the
update-event flow to guard against a missing calendar association before
accessing calendar.user_id, and verify that the CalendarEvent model defines the
included calendar alias consistently with calendar.destroyEvent.js. Preserve the
existing userId authorization behavior for events with a valid association.
In `@server/lib/calendar/calendar.upsertCalendars.js`:
- Around line 21-33: Update upsertCalendars to enforce MAX_CALENDARS_PER_USER
for the user before creating additional calendars, including calendars across
all services rather than filtering by service_id. Reuse the existing
limit/configuration and ensure the check accounts for the calendars being
upserted without rejecting updates to existing records.
In `@server/lib/calendar/calendar.upsertEvents.js`:
- Around line 9-13: Update the JSDoc description near the module header to match
the implemented overlap rule documented by the inline comment: use the
conditional end-time comparison and the start-time comparison for events without
an end. Capitalize the sentence beginning “optionally prune” to resolve the lint
warning, then run npm run eslint-fix from the server directory.
- Around line 106-135: Validate the required prunePrefix argument at the start
of the window-handling branch in the calendar upsert flow, before querying or
filtering candidates. When window is provided without a valid prunePrefix,
reject the call explicitly instead of allowing startsWith to receive undefined;
preserve existing pruning behavior when both arguments are supplied.
- Around line 45-48: Validate event.external_id at the start of
calendar.upsertEvents, before the db.CalendarEvent.findOne lookup, and reject
missing or empty values. Preserve the existing lookup and update behavior for
valid external_id values.
In
`@server/lib/external-integration/externalIntegration.disableCalendarAccount.js`:
- Around line 26-32: Update the notification logic in the calendars deletion
flow to partition the deleted calendars by their prior shared status. Notify
with broadcast enabled using only shared selectors, and notify userId directly
using only private selectors; avoid including private selectors in any broadcast
payload. Follow the relevant living specification in docs/specs/ for the
notification contract.
In `@server/lib/external-integration/externalIntegration.getCalendarAccount.js`:
- Around line 43-48: Remove the caught error argument from the logger.warn calls
handling JSON.parse failures in getCalendarAccount and the variables.forEach
callback: update
server/lib/external-integration/externalIntegration.getCalendarAccount.js lines
43-48 and
server/lib/external-integration/externalIntegration.getCalendarAccounts.js lines
30-35. Keep the existing warning messages and fallback behavior unchanged.
In
`@server/lib/external-integration/externalIntegration.publishCalendarEvents.js`:
- Around line 63-66: Update the `creator` association include in the
`db.Calendar.findOne` call to be required, ensuring calendars without an owner
are excluded before `calendar.creator.selector` is accessed. Apply the same
required-include change to the corresponding calendar query in
`externalIntegration.getIntegrationCalendars.js`.
In `@server/lib/external-integration/manifest.schema.json`:
- Around line 148-166: Update the account_schema item constraints in the
manifest schema, alongside the existing configField and oauth2 restriction, to
reject {{port:<name>}} placeholders in section labels and descriptions. Match
the validation behavior implemented by externalIntegration.validateManifest.js
and use the corresponding docs/specs contract as the source of truth, ensuring
manifests accepted by the schema cannot later be rejected during installation.
---
Outside diff comments:
In `@server/lib/calendar/calendar.updateEvent.js`:
- Line 33: Restrict the payload passed to existingCalendarEvent.update in the
event update flow to approved editable event fields, excluding calendar_id and
other ownership-sensitive attributes. Preserve the existing ownership validation
and add a regression test confirming a user cannot reassign an event to another
calendar.
---
Nitpick comments:
In
`@front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx`:
- Around line 26-32: Extract the shared no-device-screen manifest-type list into
an exported constant, then import and reuse it at
front/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsx
lines 26-32, device-page/index.js lines 16-18, discover-page/index.js lines
44-46, and front/src/routes/integration/index.js lines 227-230. Replace each
inline array, and reuse the constant for the card type mappings at lines 244 and
266 in front/src/routes/integration/index.js.
In `@server/lib/calendar/calendar.upsertEvents.js`:
- Line 6: Move MAX_EVENTS_PER_CALENDAR from calendar.upsertEvents.js into the
shared external-integration constants module alongside MAX_CALENDARS_PER_USER
and MAX_CALENDAR_EVENTS_PER_REQUEST, then update the event upsert code to import
and use the shared constant.
In
`@server/test/controllers/externalIntegration/externalIntegration.calendar.test.js`:
- Around line 49-53: Add an afterEach hook in the “External integration
management API: my calendars” suite to clear calendarWriteRateLimits and call
gladys.externalIntegration.clearTimers() after every test, preventing shared
rate-limit state and pending timers from leaking between tests.
In `@server/test/controllers/integrationHost/integrationHost.calendar.test.js`:
- Around line 142-166: Track every service created by these tests in the
existing cleanup mechanism, including deviceService and otherService, by adding
each seeded service ID to seededServiceIds. Update the existing afterEach to
clearTimers for all tracked IDs, and remove the inline
clearTimers(otherService.id) call so cleanup also runs when assertions fail.
- Around line 245-259: Update the rate-limit test around “should rate limit the
write endpoints to 30 per minute” to control time with a stubbed Date.now or
fake clock, keeping all 31 requests within the same 60-second window. Restore
the clock after the test so other integration tests retain normal time behavior.
In `@server/test/lib/calendar/calendar.upsertCalendars.test.js`:
- Around line 55-64: Update the test case around calendar.upsertCalendars to
submit a new calendar before the conflicting calendar owned by USER_A, ensuring
the conflict occurs after an attempted write. Assert that the existing calendar
remains unchanged and the new calendar is absent from the database, verifying
the whole batch was rolled back.
In `@server/test/lib/external-integration/externalIntegration.calendar.test.js`:
- Around line 218-223: In the test around the sendCalls filtering, assert that
sendCalls contains a matching call before accessing sendCalls[sendCalls.length -
1].args. Mirror the existing length guard used by the sibling tests, then retain
the lastPayload and calendar_selectors assertions.
- Around line 193-197: Update the assertions for filtered CALENDAR.UPDATED
websocket.send-all calls in the affected tests to use the deterministic expected
counts instead of greaterThan lower bounds. Adjust both assertions near the
first update scenario and the later scenario so duplicate pushes cause failure,
while preserving the existing event filtering.
In `@server/test/lib/external-integration/externalIntegration.uninstall.test.js`:
- Around line 55-62: Import WEBSOCKET_MESSAGE_TYPES from the established
constants module, then update the calendarPushes filter to compare against
WEBSOCKET_MESSAGE_TYPES.CALENDAR.UPDATED instead of the hardcoded
'calendar.updated' string.
In `@server/test/lib/external-integration/testUtils.test.js`:
- Around line 108-123: The calendar manifest fixture is duplicated across three
test files; keep TEST_CALENDAR_MANIFEST in
server/test/lib/external-integration/testUtils.test.js:108-123 as the single
source and ensure it remains importable from the controller tests. In
server/test/controllers/externalIntegration/externalIntegration.calendar.test.js:7-19,
replace CALENDAR_MANIFEST with the shared import and apply per-test changes via
object spreading; make the same replacement in
server/test/controllers/integrationHost/integrationHost.calendar.test.js:9-18,
preserving the shared app_password field.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc768973-0a79-4604-8e19-0faa464322c9
📒 Files selected for processing (60)
AGENTS.mddocs/specs/external-integrations.mdfront/src/actions/calendar.jsfront/src/config/i18n/de.jsonfront/src/config/i18n/en.jsonfront/src/config/i18n/fr.jsonfront/src/routes/calendar/index.jsfront/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsxfront/src/routes/integration/all/external-integration/config-page/CalendarAccountCard.jsxfront/src/routes/integration/all/external-integration/config-page/ConfigTab.jsxfront/src/routes/integration/all/external-integration/config-page/index.jsfront/src/routes/integration/all/external-integration/device-page/index.jsfront/src/routes/integration/all/external-integration/discover-page/index.jsfront/src/routes/integration/all/external-integration/install-page/index.jsfront/src/routes/integration/index.jsserver/api/controllers/calendar.controller.jsserver/api/controllers/externalIntegration.controller.jsserver/api/controllers/integrationHost.controller.jsserver/api/routes.jsserver/lib/calendar/calendar.createEvent.jsserver/lib/calendar/calendar.destroy.jsserver/lib/calendar/calendar.destroyEvent.jsserver/lib/calendar/calendar.update.jsserver/lib/calendar/calendar.updateEvent.jsserver/lib/calendar/calendar.upsertCalendars.jsserver/lib/calendar/calendar.upsertEvents.jsserver/lib/calendar/index.jsserver/lib/external-integration/constants.jsserver/lib/external-integration/externalIntegration.calendarNotify.jsserver/lib/external-integration/externalIntegration.deleteIntegrationCalendar.jsserver/lib/external-integration/externalIntegration.disableCalendarAccount.jsserver/lib/external-integration/externalIntegration.getCalendarAccount.jsserver/lib/external-integration/externalIntegration.getCalendarAccounts.jsserver/lib/external-integration/externalIntegration.getIntegrationCalendars.jsserver/lib/external-integration/externalIntegration.publishCalendarEvents.jsserver/lib/external-integration/externalIntegration.publishCalendars.jsserver/lib/external-integration/externalIntegration.saveCalendarAccount.jsserver/lib/external-integration/externalIntegration.uninstall.jsserver/lib/external-integration/externalIntegration.updateUserCalendar.jsserver/lib/external-integration/externalIntegration.validateManifest.jsserver/lib/external-integration/index.jsserver/lib/external-integration/manifest.schema.jsonserver/lib/index.jsserver/models/calendar.jsserver/services/caldav/lib/calendar/calendar.formaters.jsserver/services/caldav/lib/calendar/calendar.requests.jsserver/services/caldav/lib/calendar/calendar.syncUserCalendars.jsserver/services/caldav/lib/calendar/calendar.syncUserWebcals.jsserver/test/controllers/calendar/calendar.ownership.test.jsserver/test/controllers/externalIntegration/externalIntegration.calendar.test.jsserver/test/controllers/integrationHost/integrationHost.calendar.test.jsserver/test/controllers/request.test.jsserver/test/lib/calendar/calendar.ownership.test.jsserver/test/lib/calendar/calendar.upsertCalendars.test.jsserver/test/lib/calendar/calendar.upsertEvents.test.jsserver/test/lib/external-integration/externalIntegration.calendar.test.jsserver/test/lib/external-integration/externalIntegration.uninstall.test.jsserver/test/lib/external-integration/externalIntegration.validateManifest.test.jsserver/test/lib/external-integration/testUtils.test.jsserver/utils/constants.js
💤 Files with no reviewable changes (1)
- server/models/calendar.js
…ound The prune window overlap became exclusive on the end side, but the JSDoc of upsertEvents still described the previous `(end ?? start) >= from` test, contradicting the implementation just below it. Autofix-Pass: 1
There was a problem hiding this comment.
Stale comment
Re-review at
8fae0d6(after the invalidation / prettier / exclusive-end follow-ups).Prior blockers from
da71036are cleared. Verified in-tree: 10k cap on the move path,sharedtrue→false snapshotswasSharedbefore notify, event moves push source∪destination selectors, uninstall notifies previous viewers, exclusive-end overlap, and calendar is included in install / discover / device exclusions. Soft docs residuals (exclusive-end formula, complete invalidation set) are addressed too. Living-spec contracts for user-scoped ids, ownership on user calendar writes, empty-schema Enable, rate limits, and tenant isolation still look solid.Not ready to approve yet:
codecov/patchfailing at 94.53% (target 100%) — ~76 uncovered lines, mostly validation/error paths inpublishCalendarEvents/publishCalendars/ account helpers. This will block merge once the suite is green.updateEventownership is incomplete —userIdis checked, butreq.bodyis still mass-assigned, socalendar_idcan move an owned event onto another user's calendar (inline).Soft / non-blocking:
- Latest Server test red is an unrelated flaky
zigbee2mqttManager.publishsinon assertion (5701 passing otherwise; prior22e4331run was green).- PR title/body still read as docs-only while milestone 1 is implemented.
disable/uninstallSEND_ALL with a mixed private+shared selector list can expose private selectors to other connected users when any touched calendar is shared — consider partitioning shared vs private notifies.- Returning nested
calendar: { user_id }fromPATCH /calendar/event/:selectorvia the ownershipincludemay be an accidental API shape change.No new
DEVICE_FEATURE_CATEGORIES/TYPES. Not taggingrisk:high(additive external-integration surface, no migration). Keepingneeds:human-review+atrovatofor the trust-boundary / philosophy call (unaudited container → calendars →shared→ scene triggers). Removingneeds:cursor-review.Sent by Cursor Automation: Automatic PR review
Address the review findings on the calendar external integrations: - updateEvent: a user-initiated update writes only the editable fields, so owning an event is no longer a way to move it onto someone else's calendar; the calendar include becomes an INNER JOIN, an event without its calendar is "not found" instead of a dereference of null. - upsertEvents: reject an empty external_id (the lookup would otherwise match the manually created events) and a window without prunePrefix (startsWith(undefined) silently matched nothing). - disableCalendarAccount: partition the deletion notification by prior visibility, the selectors of private calendars no longer reach the broadcast. - getCalendarAccount(s): drop the parse error from the log line, its message embeds a snippet of the value, which carries the secrets. - getIntegrationCalendars: reject a non-string user filter before it reaches Sequelize. - front: derive the optimistic calendar toggle and its rollback from the previous state, two toggles fired back to back no longer drop each other. Cover every line the branch adds in server/lib/calendar and server/lib/external-integration, including the reject paths of publishCalendars and publishCalendarEvents. Autofix-Pass: 2
There was a problem hiding this comment.
Stale comment
Re-review of
6f7d9a9c(after the calendar-primitive / host-API hardening).Previous blockers are cleared.
updateEventnow restricts user-initiated writes toUSER_EDITABLE_FIELDS(ownership columns stay out of reach; CalDAV with nouserIdstill writes the full row).codecov/patchis green. CI is green (server tests, front test/build, Cypress, Docker).The rest of the B.19 surface looks solid: user-scoped
ext:<selector>:<user>:ids, tenant isolation, empty-schema Enable, exclusive-end overlap prune, 10k cap on the move path,sharedtrue→false snapshot, event-move notifying source ∪ destination, front exclusions forcalendar, no new device categories.Remaining nits (not merge blockers; left inline):
- Uninstall still broadcasts mixed private+shared selectors on
SEND_ALL—disableCalendarAccountwas partitioned in this commit for that leak.calendar.updatestill mass-assignsreq.bodyafter the new ownership check (same patternupdateEventjust closed).Not
risk:high: additive calendar host/management API, no auth/gateway/crypto/migration. Keepingneeds:human-reviewfor the product calls (push model,sharedas the scene boundary, per-user consent).Sent by Cursor Automation: Automatic PR review
Uninstall broadcast every selector of a user on SEND_ALL as soon as one of their calendars was shared, leaking the selectors of their private ones to every connected user. It now partitions by prior visibility like disableCalendarAccount does: shared selectors on SEND_ALL, private ones on SEND to their owner. A user-initiated calendar update wrote the whole body, so an owner could PATCH user_id / service_id / selector / external_id and hand the calendar — and the events it carries — over to someone else. It now writes an explicit field list, mirroring USER_EDITABLE_FIELDS on events; internal callers (no userId) keep writing the full row. Autofix-Pass: 3
There was a problem hiding this comment.
Re-review of 16c09d63 (after the uninstall-partition / calendar-update whitelist follow-up).
Prior nits from 6f7d9a9c are cleared. Uninstall now partitions shared vs private selectors the same way disableCalendarAccount does (empty arrays are a no-op in notifyCalendarUpdated). User-initiated calendar.update writes USER_EDITABLE_FIELDS only (name / description / color / sync / shared / notify), so an owner can no longer PATCH user_id / service_id / selector / external_id. Both paths have tests.
Blocker: the branch is CONFLICTING with master. A merge today fails in four files, and two of them are not mechanical:
front/src/routes/integration/index.js— master (#2860) replaced type-basedEXTERNAL_CATEGORIES/card.type === categorywith browsecategoriesfrom the store index. This branch still filters the catalog bymanifest.typeand adds'calendar'. A naive take of either side either undoes catalog categories or drops calendar’s non-admin visibility and config-page URL.docs/specs/external-integrations.md— C.4’scalendar.account-updatedrow collides with #2879’saccount_linkoauth wording; C.5’s store row is missing master’scategories/first_seen_at.server/lib/external-integration/index.js—calendarWriteRateLimitsvs master’snetworkWakeTimes(#2864): keep both.server/utils/constants.js—CALENDAR_TYPESvsINTEGRATION_CATALOG_CATEGORIES: keep both.
Please rebase/merge master and resolve those by combining both sides, then we can re-approve.
Not this PR: the red “Server test” on this SHA is Device.migrate “should move the whole history in slices” hitting the 2s timeout — unrelated flake; Mocha reported 5721 passing / 1 failing. Front, Cypress, Docker, and lint are green.
Still good, unchanged: user-scoped ext:<selector>:<user>: ids; host-API tenant isolation; empty-schema Enable; overlap prune with exclusive end; 10k cap on the move path; shared true→false snapshot notify; event-move notifies source ∪ destination; no new device feature categories (only CALENDAR_TYPES / calendar.updated, brand-agnostic). No DB migration.
Verdict: not ready to merge until the conflicts are resolved. Not risk:high. Keeping needs:human-review + atrovato: B.19 is a new integration type with per-user accounts, a shared automation boundary, and a catalog-placement question that now has to be answered against #2860’s browse categories.
Sent by Cursor Automation: Automatic PR review
| const EXTERNAL_CATEGORIES = ['device', 'communication', 'weather']; | ||
| // type ("device", "communication", "weather" or "calendar"), and can | ||
| // also be favorites | ||
| const EXTERNAL_CATEGORIES = ['device', 'communication', 'weather', 'calendar']; |
There was a problem hiding this comment.
git merge origin/master conflicts here: master (#2860) dropped type-based EXTERNAL_CATEGORIES and filters the sidebar with card.categories from the store index (getCardCategories, firstSeenAt). This branch still keys the catalog on manifest.type (card.type === category) and adds 'calendar' to that list.
A naive resolution either undoes the catalog-categories model or drops the B.19 bits (non-admin visibility, config-page URL, type: 'calendar'). The merge needs both:
- keep master’s
getCardCategories/categories/firstSeenAtand the sidebar filter onintegration.categories - keep this branch’s
'calendar'in the non-admin installed filter and ingetInstalledUrl(no Devices/Discovery screens)
How a type: "calendar" integration lands in the existing Calendar catalog category (calendars.json) vs a browse category from the indexer is the remaining product call — B.19 currently says it maps into Calendar.
| | core → integration | `external-integration.weather.get` | `{ "message_id": "uuid", "options": { "latitude": 48.85, "longitude": 2.35, "language": "fr", "units": "metric" \| "us" } }` — the core asks a "weather" integration (B.18) for the weather; respond via `command-result` with `data: { "weather": <pivot format, B.18> }`, values in the requested unit system; ack expected within **15 s** (a third-party API call, same exception as `camera.get-image`); the payload is normalized and bounded by the core before use | | ||
| | core → integration | `external-integration.weather.get-image` | `{ "message_id": "uuid", "key": "vigilance-map" }` — the core asks for a provider image declared in the pivot's `images` (B.18 point 6); respond via `command-result` with `data: { "image": "<raw base64, no data-URI prefix>" }` — decoded bytes ≤ 500 KB, PNG or JPEG only (magic numbers checked by the core); ack within **15 s**; the core caches the validated image 10 min and only ever sends a `key` declared in the integration's last normalized weather payload (undeclared keys 404 core-side) | | ||
| | integration → core | `external-integration.weather.refresh` | `{}` — freshness nudge (B.18 point 5): "re-pull me now and re-evaluate the alert scene triggers"; no `message_id`, no ack, fire-and-forget; accepted from "weather" integrations only; rate-limited to 1/min per integration, silently dropped beyond | | ||
| | core → integration | `external-integration.calendar.account-updated` | `{ "user": "<user_selector>" }` — a user enabled or disabled the integration, changed their account values, or toggled a calendar's `sync`/`shared` ("calendar" type, B.19); no `message_id`, no ack (the `config-updated` family), emitted **after** the core applied the change (on a disable, the user's calendars are already destroyed); the integration re-reads `GET /calendar/account` and `GET /calendar` and adjusts its sync loops | |
There was a problem hiding this comment.
Conflicts with master: this row and the following oauth.get-authorize-url row occupy the same slot as #2879’s account_link wording (redirect_uri absent for account_link). Keep both — calendar.account-updated stays, and the oauth row must keep master’s oauth2 / account_link distinction.
| | `GET .../:selector` | → detail (same fields, + the main container's `"started_at"`, + `"connection_status": { "connected", "message" }` (C.3), + `"containers": [ { "name", "status", "desired", "started_at", "ports": [{ "container_port", "protocol", "host_port", "label", "name", "browsable" }], "devices": [{ "class", "granted", "available" }] } ]` for multi-container ones — the same state as `GET /container` (C.3), from which the frontend derives the "Open" links (`browsable: false` → host port displayed without link) and the `{{port:<name>}}` placeholder values (C.1)); **non-admin**: the reduced view on a communication integration, `404` on any other (indistinguishable from an unknown selector) | | ||
| | `GET /api/v1/external_integration` | → `[ { "id", "name", "selector", "status", "version", "docker_image", "store_slug", "manifest", "update_available" } ]`; **non-admin**: only the installed `type: "communication"` and `type: "calendar"` integrations, in their reduced view | | ||
| | `GET .../:selector` | → detail (same fields, + the main container's `"started_at"`, + `"connection_status": { "connected", "message" }` (C.3), + `"containers": [ { "name", "status", "desired", "started_at", "ports": [{ "container_port", "protocol", "host_port", "label", "name", "browsable" }], "devices": [{ "class", "granted", "available" }] } ]` for multi-container ones — the same state as `GET /container` (C.3), from which the frontend derives the "Open" links (`browsable: false` → host port displayed without link) and the `{{port:<name>}}` placeholder values (C.1)); **non-admin**: the reduced view on a communication or calendar integration, `404` on any other (indistinguishable from an unknown selector) | | ||
| | `GET /api/v1/external_integration/store` *(admin)* | → `{ "refreshed_at", "integrations": [ { "store_slug", "manifest": <manifest>, "github": { "stars", "pushed_at" }, "installed": false, "update_available": false, "compatible": true } ] }` (filtered by `gladys_version`) | |
There was a problem hiding this comment.
This store row is also a merge conflict with master (#2860): current master adds categories and first_seen_at to GET .../store. Taking this branch as-is would drop that C.5 contract. Same table: keep this branch’s non-admin type: "calendar" reduced view and master’s catalog fields.
| this.networkDiscoveryActiveScanTimes = new Map(); | ||
| // serviceId -> { count, resetAt } fixed one-minute window rate limit on | ||
| // the calendar write endpoints (POST/DELETE /calendar*) | ||
| this.calendarWriteRateLimits = new Map(); |
There was a problem hiding this comment.
Conflicts with master (#2864): networkWakeTimes is constructed in this same spot. Keep both maps — dropping either loses a rate-limit table.
| EXTERNAL: 'external', | ||
| }; | ||
|
|
||
| const CALENDAR_TYPES = { |
There was a problem hiding this comment.
Conflicts with master (#2860): INTEGRATION_CATALOG_CATEGORIES was inserted in this same place. Keep both CALENDAR_TYPES and the catalog-categories vocabulary — they are unrelated.


Description
This PR updates the external integrations specification to document the "calendar" type (B.19), enabling calendar providers (Google Calendar, Outlook, Nextcloud, CalDAV servers, and public feeds) to become ordinary external integrations that feed the same calendar view and scene triggers as the internal CalDAV service.
The specification covers:
Core prerequisites: batch upsert primitives for calendars/events, unique selector generation, ownership checks, explicit cleanup at uninstall, real-time WebSocket refresh, and drive-by fixes.
Per-user accounts: manifest
account_schema(same format as communication integrations), per-user activation as consent gesture, non-admin visibility, and automation boundary via thesharedflag.Host API: five new endpoints for account management, calendar listing, and event upsert/delete with rate limiting and tenant isolation.
Normalization and bounds: field whitelisting, length/format validation, color validation, and a 10,000-event-per-calendar cap.
Sync semantics: field ownership split (integration-owned: name/description/color; user-owned: sync/shared/selector), window-based pruning for deletions, and idempotent upsert.
Milestone 2 sketch: per-user OAuth2 support (deferred to a future milestone).
Frontend and SDK: calendar category integration, real-time refresh listener, and new SDK methods (
getCalendarAccounts,getCalendars,publishCalendars,deleteCalendar,publishCalendarEvents,onCalendarAccountUpdated).The design follows the "integration syncs, core stores" principle: integrations own the provider protocol and sync cadence; the core owns storage, normalization, and automations. This is a push model (like CalDAV) rather than pull (like weather), keeping third-party containers off the critical path of scene scheduling.
Also updates
AGENTS.mdto note that B.19 is now specified but not yet implemented.Checklist
https://claude.ai/code/session_01Jhfyd3Vz9icRe5QjuUUXVi
Summary by CodeRabbit