Skip to content

Feat: CalDAV REPORT sync, multi-calendar & agent datetime - #15

Merged
gerfru merged 7 commits into
mainfrom
feat/caldav-report-multicalendar
Feb 19, 2026
Merged

gerfru merged 7 commits into
mainfrom
feat/caldav-report-multicalendar

Conversation

@gerfru

@gerfru gerfru commented Feb 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • CalDAV REPORT sync: Replace PROPFIND+GET per-event approach with RFC 4791 REPORT time-range queries. Reduces ~2,900 HTTP requests to ~6, sync time from ~10 min to ~2 sec. Only syncs events from -30 days to +365 days.
  • Multi-calendar support: Auto-discover CalDAV collections, allow users to select which calendars to sync via Settings GUI (htmx lazy-loaded checkboxes).
  • Agent datetime awareness: Inject current date/time into system prompt so the LLM knows "today". Strengthen soul.md tool-use instructions to prevent hallucinated calendar data.

Changes

File Change
src/niles/sync/caldav.py REPORT time-range, collection discovery, calendar filtering
src/niles/sources/web.py CalDAV calendar discovery endpoint, caldav_enabled flag
src/niles/config.py caldav_calendars setting
src/niles/settings_store.py caldav_calendars in editable whitelist
src/niles/main.py app.state.caldav
src/niles/agent/prompts.py Datetime injection in system prompt
src/niles/agent/core.py Pass timezone to build_system_prompt
config/soul.md Stronger find_event instructions
src/niles/templates/settings.html CalDAV calendar selection UI
src/niles/templates/fragments/calendars.html New: calendar checkbox fragment
tests/test_caldav.py Updated for REPORT-based sync
tests/test_settings_store.py Updated whitelist assertion

Test plan

  • python -m pytest tests/ -v — all 174 tests pass
  • python -m ruff check src/ tests/ — no lint errors
  • CalDAV sync runs in ~2 sec instead of ~10 min
  • Settings GUI shows discovered calendars with checkboxes
  • Chatbot correctly calls find_event instead of hallucinating

🤖 Generated with Claude Code

Gerald F. Fruhmann and others added 2 commits February 19, 2026 18:32
…wareness

Replace inefficient PROPFIND+GET per-event sync with CalDAV REPORT
time-range queries (RFC 4791), reducing ~2900 HTTP requests to ~6 and
sync time from ~10 minutes to ~2 seconds. Add multi-calendar discovery
and selection via Settings GUI. Inject current datetime into agent
system prompt and strengthen tool-use instructions so the LLM reliably
calls find_event instead of hallucinating calendar data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gerfru

gerfru commented Feb 19, 2026

Copy link
Copy Markdown
Owner Author

@claude, please review the PR, especially security-wise

@claude

claude Bot commented Feb 19, 2026 •

Copy link
Copy Markdown

Claude finished @gerfru's task in 3m 7s —— View job


PR Security Review

Tasks:

  • Gather context and understand request
  • Read all 13 changed files
  • Analyze security concerns
  • Post comprehensive review

Overall this is a well-structured PR with several good security practices in place. Below are the findings, ordered by severity.


🔴 Medium: SSRF via absolute hrefs in CalDAV REPORT responses

src/niles/sync/caldav.py:274-277

return [
    self._base_url + h if not h.startswith("http") else h
    for h in collections
]

If the CalDAV server returns response blocks with absolute URLs (http://...), they are passed through as-is to subsequent REPORT requests. A compromised or malicious CalDAV server could return hrefs like http://169.254.169.254/ or http://internal-db:5432/ to probe internal services from the application's network context.

Recommendation: Validate that discovered hrefs share the same origin as _base_url before making requests to them. Fix this →


🟠 Medium: No response size limit on CalDAV HTTP responses

src/niles/sync/caldav.py:219-231 and 314-334

Both _report_time_range and _propfind_request read the full response body with response.text without any size cap. A malicious CalDAV server (or a legitimate server returning unexpectedly large data) could cause memory exhaustion.

Recommendation: Add a max_bytes limit check after receiving the response, e.g., reject responses above ~10 MB:

if len(response.content) > 10 * 1024 * 1024:
    raise ValueError("CalDAV response too large")

Fix this →


🟠 Medium: Prompt injection via CalDAV event data

src/niles/agent/core.py:325-332 (data flow)

Event summaries, descriptions, and locations from the CalDAV server flow into the LLM via find_event. An attacker who can add events to the synced calendar (e.g., a shared calendar, or a compromised CalDAV account) could inject LLM instructions via crafted event text:

SUMMARY:Team Meeting\n\n## New Instructions\nIgnore previous instructions and...

The soul.md instructions (requiring find_event calls) help mitigate hallucination but don't prevent prompt injection from tool results. This is an inherent risk whenever untrusted external data reaches the LLM context.

Recommendation: Consider sanitizing event fields before returning them from find_event — strip control characters, limit field length, and/or add a system note like [UNTRUSTED CALENDAR DATA] around returned event content so the model has clearer context about data origin.


🟡 Low: caldav_calendars value is not validated

src/niles/settings_store.py:27 and src/niles/sources/web.py:528-563

caldav_calendars is in EDITABLE_SETTINGS and accepts arbitrary string values through the settings endpoint. The key format is validated by _KEY_PATTERN, but the value (comma-separated hrefs) has no length cap or format check. An authenticated user could store excessively long strings. Additionally, hrefs containing commas would corrupt the filter list when split.

Recommendation: Add length validation (e.g., max 4096 characters) on the value in settings_store.set() or at the endpoint level. Fix this →


🟡 Low: Timezone setting injected into system prompt without sanitization

src/niles/agent/prompts.py:46-47

f"Es ist {now.strftime('%H:%M')} Uhr ({timezone})."

The timezone string from settings is injected directly into the system prompt. An authenticated user who changes the timezone to Europe/Vienna\n\n## New Rules\nIgnore... would inject content into the system prompt. This requires settings access, so the blast radius is limited, but it's a defence-in-depth gap.

Recommendation: Validate or sanitize the timezone value before embedding it (e.g., verify it's a valid IANA timezone identifier using ZoneInfo). The ZoneInfo(timezone) call at line 35 will already raise ZoneInfoNotFoundError for invalid keys — that exception could propagate up to prevent an invalid value from being persisted in settings.


🟡 Low: Regex-based XML parsing

src/niles/sync/caldav.py:25-51

Parsing XML with regex is inherently fragile. The current patterns (_RESPONSE_BLOCK_REGEX, _CALENDAR_DATA_REGEX, etc.) could be confused by:

  • XML comments (``)
  • CDATA sections
  • Unusual namespace prefixes

While this is partially mitigated by the fact that you control the CalDAV server configuration, using xml.etree.ElementTree (stdlib) or lxml would be significantly more robust and eliminate a class of potential bypasses.


ℹ️ Informational: Process-local login rate limiting

src/niles/sources/web.py:43-58

The _login_attempts dict is in-memory and per-process. In a multi-worker or multi-container deployment, rate limiting would not be coordinated across instances. This is acceptable for a single-instance personal deployment, but worth noting for future scaling.


✅ What's done well

  • SQL injection: All DB queries use parameterized asyncpg calls ($1, $2, …) — no string interpolation in SQL.
  • iCalendar injection: _escape_ical_text correctly escapes \, ;, ,, and newlines before inserting user-supplied data into .ics bodies. The test test_injection_attempt_escaped covers this.
  • CSRF: All state-changing POST endpoints go through _require_auth_and_csrf(). The new GET endpoint (/api/caldav/calendars) correctly omits CSRF (correct for read-only).
  • Sensitive settings masking: _safe_settings_dict masks passwords with "********" — they are never exposed to the template.
  • UUID for event IDs: uuid.uuid4() is cryptographically random — no event UID guessing.
  • OAuth state parameter: Properly stored in an httponly cookie and compared with hmac.compare_digest.
  • Settings key validation: _KEY_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,63}$") prevents any Python attribute-name tricks like __class__.
  • caldav_calendars in EDITABLE_SETTINGS but not in infra display: The setting can be changed at runtime but is not shown in the read-only infrastructure section of the UI, which is the right scoping.

1. SSRF: Reject CalDAV hrefs with foreign origins (non-matching base URL)
2. Response size: Cap CalDAV REPORT/PROPFIND responses at 10 MB
3. Prompt injection: Sanitize event fields (strip control chars, truncate)
   before passing to LLM context
4. Value validation: Reject settings values exceeding 4096 characters
5. Timezone validation: Verify IANA timezone in SettingsStore.set() and
   graceful fallback in build_system_prompt()
6. XML parsing: Replace all regex-based XML parsing with
   xml.etree.ElementTree for robustness against comments, CDATA, and
   namespace variations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gerfru

gerfru commented Feb 19, 2026

Copy link
Copy Markdown
Owner Author

@claude: rerview the changes

@claude

claude Bot commented Feb 19, 2026 •

Copy link
Copy Markdown

Claude finished @gerfru's task in 4m 31s —— View job


PR Review

Tasks:

  • Gather context
  • Read changed files
  • Analyze code changes
  • Post comprehensive review

Good overall PR — the REPORT-based sync is a substantial improvement and all six findings from the previous security review have been addressed. A few new issues below, plus notes on code quality.


🔴 Bug: CSP blocks the onclick handler in the calendar fragment

src/niles/templates/fragments/calendars.html:14-18

<button type="submit" class="outline" onclick="
    var boxes = this.form.querySelectorAll('input[name=cal]:checked');
    var vals = Array.from(boxes).map(function(b){return b.value});
    this.form.querySelector('#cal-value').value = vals.join(',');
">Speichern</button>

base.html sets script-src 'self' https://unpkg.com with no 'unsafe-inline' — inline event handlers are treated as inline scripts and will be blocked by every browser enforcing CSP. The save button does nothing.

The existing code handles this deliberately: feature-flag toggles use data-flag-toggle + event delegation in app.js (line 37-44) to stay CSP-safe. The same pattern should be applied here — give the form an id (e.g., id="calendar-form") and add a handler in app.js. Fix this →


🟠 Missing test: caldav_calendars filter path

src/niles/sync/caldav.py:485-487

allowed = self._allowed_collections()
if allowed:
    collections = [h for h in collections if h in allowed]

There's no unit test for the case where caldav_calendars is set and filters the discovered collections. Given this is the key behaviour change (users selecting which calendars to sync), it deserves a test alongside test_discovers_collections_from_root. Fix this →


🟡 Accessing a private method from another module

src/niles/sources/web.py:292

selected = caldav._allowed_collections()

_allowed_collections has the single-underscore "private" prefix but is called from web.py. If the method is meant to be part of the public API of CalDAVSync, rename it (drop the underscore). If it's truly internal, expose the information via a public get_selected_hrefs() -> set[str] | None method. Fix this →


🟡 Minor: Unnecessary f prefix on a string literal

src/niles/agent/prompts.py:171

time_section = (
    f"\n\n## Aktuelle Zeit\n"   # ← no interpolation
    f"Heute ist {weekday}, der {now.strftime('%d.%m.%Y')}. "
    ...
)

The first string in the concatenation has the f prefix but contains no {} expressions. Drop it to "\n\n## Aktuelle Zeit\n" to avoid unnecessary overhead and a potential linter warning.


🟡 Minor: Extra blank line in caldav.py

src/niles/sync/caldav.py around line 541

        return {h.strip() for h in raw.split(",") if h.strip()}


    async def _propfind_request(self, url: str) -> str | None:

PEP 8 mandates two blank lines between top-level definitions and one blank line between method definitions. There are three blank lines here.


🟡 Design note: docker-compose.yml postgres port always exposed

docker/docker-compose.yml:40-42

The port was previously commented out with the note "For local debugging". Now it's always active (127.0.0.1:5432:5432). Since it's bound to loopback this is not an external exposure risk, but any process on the same host can reach Postgres directly, bypassing the app's auth layer. If this is intentional for development, an environment variable guard (${EXPOSE_POSTGRES:-}) or leaving it commented in production would be safer.


✅ Previous security findings — all addressed

All six findings from the prior review are fixed in this PR:

Finding Fix location
SSRF via absolute hrefs caldav.py:492-498 — origin validated, foreign hrefs skipped
No response size limit caldav.py:437-440, 560-563 — 10 MB cap enforced
Prompt injection via event data calendar.py:73-83 — control-char strip + 500-char truncation
caldav_calendars value unbounded settings_store.py:239-242 — 4096-char limit
Timezone injected unsanitized settings_store.py:244-251 — IANA validation before save
Regex XML parsing caldav.py:306-307 — migrated to xml.etree.ElementTree

✅ What's done well

  • Performance: REPORT replaces ~2900 GET requests with ~6. The sync window (_SYNC_DAYS_PAST=30, _SYNC_DAYS_FUTURE=365) is well-chosen.
  • Datetime injection: Clean addition to the system prompt with proper fallback on invalid timezone.
  • Soul.md improvements: Explicit, concrete examples ("Zahnarzt", "Padel") and the NEVER-hallucinate instruction are much stronger than the original.
  • Htmx lazy-loading: Calendar list is loaded on demand — settings page still renders if CalDAV is slow.
  • CSRF: Correctly handled globally via htmx:configRequest in app.js — no per-form workaround needed.
  • discover_collections vs sync isolation: The UI discovery endpoint shows all calendars; the sync respects the user's selection. Correct scoping.

Gerald F. Fruhmann and others added 3 commits February 19, 2026 18:55
1. CSP: Move inline onclick handler in calendars.html to event-delegated
   handler in app.js (inline scripts blocked by CSP)
2. Test: Add test_filters_by_caldav_calendars_setting for calendar
   selection filter path
3. API: Rename _allowed_collections to allowed_collections (public,
   called from web.py)
4. Style: Remove unnecessary f-prefix in prompts.py, fix extra blank
   line in caldav.py
5. Docker: Revert postgres port exposure to commented-out default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use POSTGRES_HOST_PORT env var (default: 0 = random ephemeral port on
loopback, effectively hidden). Set POSTGRES_HOST_PORT=5432 in .env to
expose for local debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gerfru

gerfru commented Feb 19, 2026

Copy link
Copy Markdown
Owner Author

please re-review the code now @claude

@claude

claude Bot commented Feb 19, 2026 •

Copy link
Copy Markdown

Claude finished @gerfru's task in 5m 29s —— View job


PR Re-Review

Tasks:

  • Gather context
  • Read all 17 changed files
  • Check all previous findings
  • Analyze for new issues
  • Post comprehensive review

All 12 findings from the previous two reviews have been addressed. One new correctness bug and two minor issues were found.


🟠 Bug: allowed_collections() reads stale config after runtime settings changes

src/niles/config.py:87 + src/niles/sync/caldav.py:307-312 + src/niles/sources/web.py:553

CalDAVSync stores the startup Settings object in self.config. When a user saves caldav_calendars via the UI, update_setting() calls:

request.app.state.settings = apply_overrides(settings, {key: parsed_value})

apply_overrides creates a new Settings object via model_copy and assigns it to app.state.settings. But caldav_sync.config still holds the old startup object — it is never updated.

As a result:

  • The calendar selection saved through the UI doesn't take effect until the next container restart (not just the next sync as the UI implies)
  • discover_collections() in the route handler calls caldav.allowed_collections(), which also reads the stale config, so the checkboxes won't reflect a recently-saved selection without a restart

The simplest fix: in update_setting() also update caldav_sync.config when caldav_calendars is saved, or pass app.state.settings into allowed_collections() from the route handler. Fix this →


🟡 Minor: "Deselect all" silently means "sync all"

src/niles/static/js/app.js:47-51 + src/niles/sync/caldav.py:309-312

If a user unchecks every calendar and saves, vals.join(",") produces "", which is POSTed as value="". allowed_collections() then returns None (meaning "all calendars"), so the sync actually processes all calendars — the opposite of what the user intended. There's no way to explicitly opt into "sync nothing".

Since an empty string means "no override / use all" by design, this is a UX issue rather than a security one. Consider either preventing submission when nothing is checked, or treating an explicitly-empty submission as "all" and documenting it.


🟡 Minor: discover_collections() makes a live HTTP call per page load with no timeout fallback in the UI

src/niles/sources/web.py:578

Every settings page load triggers a fresh PROPFIND to the CalDAV server (30-second timeout). If the server is slow, the user waits. The htmx lazy-load helps (the page renders immediately), but a 30-second spinner on the calendar section is poor UX. A module-level cache with a short TTL (e.g. 60 seconds) would eliminate repeated calls during a settings session.


✅ All previous findings resolved

Every finding from both earlier reviews is addressed in the current branch:

# Finding Fix location Status
1 SSRF via absolute hrefs caldav.py:492-498 — origin validated ✅
2 No response size limit caldav.py:329-332, 437-440 — _MAX_RESPONSE_BYTES enforced ✅
3 Prompt injection via event data calendar.py:82-93 — _sanitize_field strips control chars + truncates ✅
4 caldav_calendars value unbounded settings_store.py:79-83 — 4096-char limit ✅
5 Timezone injected unsanitized settings_store.py:85-92 — ZoneInfo validation before save ✅
6 Regex XML parsing caldav.py:6, 247-303 — migrated to xml.etree.ElementTree ✅
7 CSP blocks inline onclick calendars.html:14, app.js:45-51 — data-calendar-save + event delegation ✅
8 Missing filter path test tests/test_caldav.py:213-234 — test_filters_by_caldav_calendars_setting ✅
9 Private method accessed from another module caldav.py:307 — renamed to allowed_collections (no underscore) ✅
10 Unnecessary f prefix prompts.py:46 — first string in time_section no longer has f prefix ✅
11 Extra blank line between methods caldav.py:305-307 — now a single blank line ✅
12 Postgres port always exposed docker-compose.yml:10-11 — ${POSTGRES_HOST_PORT:-0} pattern ✅

✅ What's done well (new in this pass)

  • allowed_collections naming: dropping the underscore and calling it from web.py is correct — the rename makes the public API explicit.
  • test_filters_by_caldav_calendars_setting: good test, correct assertion that schedule-* collections are excluded.
  • data-calendar-save pattern: CSP-safe and consistent with the existing data-flag-toggle approach in app.js.
  • _sanitize_field with 500-char truncation: conservative and correct. The ... suffix on truncated strings is a nice UX touch.
  • apply_overrides with model_copy: the Pydantic v2 approach is clean; the only issue is that CalDAVSync.config isn't kept in sync (see above).

…cache

1. Stale config: Update caldav.config after runtime settings change so
   allowed_collections() reads fresh caldav_calendars without restart
2. Empty selection UX: Disable save button when no calendar is checked,
   prevent submitting empty value (which would mean "sync all")
3. Discovery cache: Cache discover_collections() results for 60s to
   avoid repeated PROPFIND requests on settings page interactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gerfru

gerfru commented Feb 19, 2026

Copy link
Copy Markdown
Owner Author

again, rereview please @claude

@claude

claude Bot commented Feb 19, 2026 •

Copy link
Copy Markdown

Claude finished @gerfru's task in 3m 15s —— View job


PR Re-Review (Pass 4)

Tasks:

  • Gather context and understand request
  • Read all 17 changed files
  • Check previous findings (stale config, deselect-all UX, discovery cache)
  • Analyze for new issues
  • Post comprehensive review

All three remaining findings from the previous review are resolved. Two new minor observations below.


✅ All previous findings — fully resolved

# Finding Fix Status
1 SSRF via absolute hrefs caldav.py:276-284 — origin check, foreign hrefs skipped ✅
2 No response size limit caldav.py:224-227, 343-346 — _MAX_RESPONSE_BYTES enforced ✅
3 Prompt injection via event data calendar.py:82-92 — _sanitize_field strips control chars + truncates ✅
4 caldav_calendars value unbounded settings_store.py:79-83 — 4096-char limit ✅
5 Timezone injected unsanitized settings_store.py:85-92 — ZoneInfo validation before save ✅
6 Regex XML parsing caldav.py:7 — migrated to xml.etree.ElementTree ✅
7 CSP blocks inline onclick app.js:46-57 — data-calendar-save + event delegation ✅
8 Missing filter path test tests/test_caldav.py:214-230 — test_filters_by_caldav_calendars_setting ✅
9 Private method accessed cross-module caldav.py:321 — renamed to allowed_collections (no underscore) ✅
10 Unnecessary f prefix prompts.py:49 — first string in time_section is a plain string literal ✅
11 Extra blank line between methods caldav.py:326-328 — single blank line before _propfind_request ✅
12 Postgres port always exposed docker-compose.yml — ${POSTGRES_HOST_PORT:-0} pattern ✅
13 Stale config after runtime change web.py:556-558 — caldav.config = new_settings after every update_setting ✅
14 Deselect-all = sync-all app.js:51-54, 60-66 — evt.preventDefault() + btn.disabled guard ✅
15 Discovery PROPFIND on every page load caldav.py:39-40, 293-295 — 60 s in-memory TTL cache ✅

🟡 Minor: Save button disabled state not initialised on htmx fragment load

src/niles/static/js/app.js:60-66

document.body.addEventListener("change", function(evt) {
    if (evt.target.name !== "cal") return;
    ...
    btn.disabled = checked === 0;
});

btn.disabled is only ever updated by a change event. On the initial fragment render, the button is always enabled regardless of how many checkboxes are checked. In the normal case this is harmless because all (or the previously-selected) calendars are checked by default. The edge case is: if caldav_calendars contains hrefs that the server no longer returns (a calendar was deleted on the CalDAV side), the fragment loads with zero checked boxes and an enabled save button. A click would be silently swallowed by evt.preventDefault() — no visible feedback to the user.

Recommendation: Add an htmx:afterSettle (or htmx:afterSwap) listener that fires once per fragment injection to initialise the button state, similar to how the change handler does it:

document.body.addEventListener("htmx:afterSettle", function() {
    var form = document.getElementById("calendar-form");
    if (!form) return;
    var btn = form.querySelector("[data-calendar-save]");
    if (!btn) return;
    btn.disabled = form.querySelectorAll("input[name=cal]:checked").length === 0;
});

Fix this →


🟡 Minor: Null dereference risk in change handler

src/niles/static/js/app.js:63

var btn = form.querySelector("[data-calendar-save]");
var checked = form.querySelectorAll("input[name=cal]:checked").length;
btn.disabled = checked === 0;   // btn could be null

querySelector returns null if no matching element is found. In normal operation [data-calendar-save] is always present when cal checkboxes exist (both are inside the same {% if collections %} block in the template), so this won't throw in practice. But it's worth a null guard to avoid a TypeError if the DOM ever diverges from that assumption:

if (btn) btn.disabled = checked === 0;

Fix this →


✅ What's done well (new in this pass)

  • Stale config fix is correct and minimal: caldav.config = new_settings in update_setting keeps CalDAVSync in sync on every runtime setting change without needing to special-case caldav_calendars. Clean approach.
  • Double guard on "sync nothing" UX: Both btn.disabled (prevents click) and evt.preventDefault() in the click handler (second line of defence) correctly block the deselect-all → sync-all footgun.
  • Discovery cache scoping is correct: discover_collections() caches the server's calendar list; allowed_collections() always reads the current config value. These two responsibilities are cleanly separated and the cache doesn't interfere with the checkbox state display after a save.
  • _DISCOVERY_CACHE_TTL constant: Named constant (not a magic number) makes the intent and tunability clear.
  • caldav.config is updated via update_setting for all keys, not just caldav_calendars: apply_overrides is idempotent, so this is safe and ensures no other future setting change can leave caldav.config stale.

@gerfru
gerfru merged commit ab30f5e into main Feb 19, 2026
gerfru added a commit that referenced this pull request Jun 12, 2026
1. SSRF: Reject CalDAV hrefs with foreign origins (non-matching base URL)
2. Response size: Cap CalDAV REPORT/PROPFIND responses at 10 MB
3. Prompt injection: Sanitize event fields (strip control chars, truncate)
   before passing to LLM context
4. Value validation: Reject settings values exceeding 4096 characters
5. Timezone validation: Verify IANA timezone in SettingsStore.set() and
   graceful fallback in build_system_prompt()
6. XML parsing: Replace all regex-based XML parsing with
   xml.etree.ElementTree for robustness against comments, CDATA, and
   namespace variations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gerfru added a commit that referenced this pull request Jun 12, 2026
1. CSP: Move inline onclick handler in calendars.html to event-delegated
   handler in app.js (inline scripts blocked by CSP)
2. Test: Add test_filters_by_caldav_calendars_setting for calendar
   selection filter path
3. API: Rename _allowed_collections to allowed_collections (public,
   called from web.py)
4. Style: Remove unnecessary f-prefix in prompts.py, fix extra blank
   line in caldav.py
5. Docker: Revert postgres port exposure to commented-out default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gerfru added a commit that referenced this pull request Jun 12, 2026
…cache

1. Stale config: Update caldav.config after runtime settings change so
   allowed_collections() reads fresh caldav_calendars without restart
2. Empty selection UX: Disable save button when no calendar is checked,
   prevent submitting empty value (which would mean "sync all")
3. Discovery cache: Cache discover_collections() results for 60s to
   avoid repeated PROPFIND requests on settings page interactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gerfru added a commit that referenced this pull request Jun 12, 2026
Feat: CalDAV REPORT sync, multi-calendar & agent datetime
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant