From dc8b4a625afe8b73709dbf05629e10dde6a68630 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 03:45:28 +0000 Subject: [PATCH 1/3] Keep local Docker state out of the build context and git Running with a ./data bind mount (instead of the named volume in docker-compose.yml) puts the Postgres data directory inside the repo. It is owned by the container's postgres user, so the next `docker compose up --build` fails with: failed to solve: error from sender: open .../data/pgdata: permission denied Exclude data/ and pgdata/ from the build context, and keep docker-compose.override.yml (the natural place for personal settings such as the Home Assistant token) out of both the image and git. Also ignore /data/ in git: it holds the SECRET_KEY_BASE file, and committing or cleaning it away would make the encrypted data unreadable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D3eqQ5cKPh9Sm6H9E7YWQu --- .dockerignore | 5 +++++ .gitignore | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/.dockerignore b/.dockerignore index 3348f55b..fa58749f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,8 @@ test .github docker-compose.yml config/timeframe.yml +# Local runtime state and personal compose settings (may hold the database, +# SECRET_KEY_BASE and a Home Assistant token) must never enter the image. +data +pgdata +docker-compose.override.yml diff --git a/.gitignore b/.gitignore index 46292b79..49a6743b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ test_full_output.txt engine/test/dummy/tmp/ # Capybara/system test screenshots tmp/screenshots/ +# Local Docker state: a ./data bind mount holds the database and the +# SECRET_KEY_BASE file; the override file holds personal settings/tokens. +/data/ +docker-compose.override.yml From d343b9732d4dbebb0c8a77e39eac3d33ad39f0c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 08:05:49 +0000 Subject: [PATCH 2/3] Ignore calendar event fields DeviceEvent doesn't accept Home Assistant now includes a `status` field (confirmed/tentative) on calendar events. fetch_calendars keeps every field HA returns apart from a fixed list, and calendar_events splats the rest into DeviceEvent.new, so the new field raises ArgumentError (unknown keyword: :status) That takes down the Events page, the Status page (which lists calendar events), and device rendering, which also goes through calendar_events. Slice each event to the keywords DeviceEvent#initialize accepts before constructing it. Doing this on read also covers events already cached before the Home Assistant upgrade, and future HA fields can't break it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D3eqQ5cKPh9Sm6H9E7YWQu --- app/apis/home_assistant_api.rb | 12 +++++++- test/apis/home_assistant_api_test.rb | 42 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/apis/home_assistant_api.rb b/app/apis/home_assistant_api.rb index ab121704..07c38fb3 100644 --- a/app/apis/home_assistant_api.rb +++ b/app/apis/home_assistant_api.rb @@ -279,12 +279,22 @@ def calendars_last_fetched_at def calendar_events @calendar_events ||= (domain_value(CALENDAR_DOMAIN)[:response] || []).map do |raw| - event = raw.symbolize_keys + event = raw.symbolize_keys.slice(*device_event_keywords) event[:description] = customized_description(event) DeviceEvent.new(**event, timezone: time_zone) end end + # The event fields DeviceEvent accepts (its initializer takes only keyword + # arguments). Home Assistant adds fields to calendar events over time (e.g. + # `status`, confirmed/tentative), and passing one DeviceEvent doesn't know + # raises ArgumentError ("unknown keyword"), which breaks the Events and Status + # pages and every device that shows events. Filtering on read also covers + # events already cached before a Home Assistant upgrade. + def device_event_keywords + @device_event_keywords ||= DeviceEvent.instance_method(:initialize).parameters.map(&:last) + end + # Merge any locally-stored per-event customization (icon/title/hide) onto the # Home Assistant event description so it applies on the rendered device. No-op # when the API has no account (e.g. isolated unit tests). diff --git a/test/apis/home_assistant_api_test.rb b/test/apis/home_assistant_api_test.rb index 40d970ab..08c7e6d6 100644 --- a/test/apis/home_assistant_api_test.rb +++ b/test/apis/home_assistant_api_test.rb @@ -851,6 +851,48 @@ def test_fetch_calendars_tags_events_with_entity_id assert_equal "calendar.family", api.calendar_events.first.entity_id end + def test_fetch_calendars_ignores_event_fields_device_event_does_not_know + api = new_test_api + calendar = {"entity_id" => "calendar.family"} + event = { + "summary" => "Dentist", + "uid" => "evt-1", + "start" => {"dateTime" => "2024-09-05T10:00:00-06:00"}, + "end" => {"dateTime" => "2024-09-05T11:00:00-06:00"}, + # Newer Home Assistant versions return an event status; future versions + # may add more fields. + "status" => "confirmed", + "some_future_field" => "x" + } + + api.stub :fetch_calendar_list, [calendar] do + api.stub :fetch_calendar_icons, {} do + HTTParty.stub :get, [event] do + api.fetch_calendars + end + end + end + + fetched = api.calendar_events.first + assert_equal "Dentist", fetched.summary + assert_equal "evt-1", fetched.id + assert_equal "calendar.family", fetched.entity_id + end + + def test_calendar_events_ignore_unknown_fields_in_already_cached_events + api = new_test_api + api.seed_calendars([{ + "summary" => "Dentist", + "starts_at" => "2024-09-05T10:00:00-06:00", + "ends_at" => "2024-09-05T11:00:00-06:00", + "entity_id" => "calendar.family", + "id" => "evt-1", + "status" => "tentative" + }]) + + assert_equal ["Dentist"], api.calendar_events.map(&:summary) + end + def test_calendar_events_apply_local_customizations account = test_user.accounts.first CalendarEventCustomization.delete_all From 004cd60eb609827aa05ea1f1ea675921a1de8297 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 3 Nov 2026 10:10:01 -0700 Subject: [PATCH 3/3] Limit pull request to ignore files Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- app/apis/home_assistant_api.rb | 12 +------- test/apis/home_assistant_api_test.rb | 42 ---------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/app/apis/home_assistant_api.rb b/app/apis/home_assistant_api.rb index 07c38fb3..ab121704 100644 --- a/app/apis/home_assistant_api.rb +++ b/app/apis/home_assistant_api.rb @@ -279,22 +279,12 @@ def calendars_last_fetched_at def calendar_events @calendar_events ||= (domain_value(CALENDAR_DOMAIN)[:response] || []).map do |raw| - event = raw.symbolize_keys.slice(*device_event_keywords) + event = raw.symbolize_keys event[:description] = customized_description(event) DeviceEvent.new(**event, timezone: time_zone) end end - # The event fields DeviceEvent accepts (its initializer takes only keyword - # arguments). Home Assistant adds fields to calendar events over time (e.g. - # `status`, confirmed/tentative), and passing one DeviceEvent doesn't know - # raises ArgumentError ("unknown keyword"), which breaks the Events and Status - # pages and every device that shows events. Filtering on read also covers - # events already cached before a Home Assistant upgrade. - def device_event_keywords - @device_event_keywords ||= DeviceEvent.instance_method(:initialize).parameters.map(&:last) - end - # Merge any locally-stored per-event customization (icon/title/hide) onto the # Home Assistant event description so it applies on the rendered device. No-op # when the API has no account (e.g. isolated unit tests). diff --git a/test/apis/home_assistant_api_test.rb b/test/apis/home_assistant_api_test.rb index 08c7e6d6..40d970ab 100644 --- a/test/apis/home_assistant_api_test.rb +++ b/test/apis/home_assistant_api_test.rb @@ -851,48 +851,6 @@ def test_fetch_calendars_tags_events_with_entity_id assert_equal "calendar.family", api.calendar_events.first.entity_id end - def test_fetch_calendars_ignores_event_fields_device_event_does_not_know - api = new_test_api - calendar = {"entity_id" => "calendar.family"} - event = { - "summary" => "Dentist", - "uid" => "evt-1", - "start" => {"dateTime" => "2024-09-05T10:00:00-06:00"}, - "end" => {"dateTime" => "2024-09-05T11:00:00-06:00"}, - # Newer Home Assistant versions return an event status; future versions - # may add more fields. - "status" => "confirmed", - "some_future_field" => "x" - } - - api.stub :fetch_calendar_list, [calendar] do - api.stub :fetch_calendar_icons, {} do - HTTParty.stub :get, [event] do - api.fetch_calendars - end - end - end - - fetched = api.calendar_events.first - assert_equal "Dentist", fetched.summary - assert_equal "evt-1", fetched.id - assert_equal "calendar.family", fetched.entity_id - end - - def test_calendar_events_ignore_unknown_fields_in_already_cached_events - api = new_test_api - api.seed_calendars([{ - "summary" => "Dentist", - "starts_at" => "2024-09-05T10:00:00-06:00", - "ends_at" => "2024-09-05T11:00:00-06:00", - "entity_id" => "calendar.family", - "id" => "evt-1", - "status" => "tentative" - }]) - - assert_equal ["Dentist"], api.calendar_events.map(&:summary) - end - def test_calendar_events_apply_local_customizations account = test_user.accounts.first CalendarEventCustomization.delete_all