diff --git a/README.md b/README.md index 3069d522..d3929455 100644 --- a/README.md +++ b/README.md @@ -34,25 +34,25 @@ These models declare the fields that can change without notifying running servic | `Module` | `updated_at`, `has_runtime_error`, `error_timestamp` | | `Driver` | `name`, `description`, `update_available`, `update_info`, `compilation_output`, `updated_at`, `search_vector` | | `Zone` | `name`, `description`, `display_name`, `playlists`, `images`, `updated_at`, `search_vector` | -| `ControlSystem` | `signage_last_seen`, `playlist_item_id`, `name`, `description`, `display_name`, `version`, `updated_at`, `playlists`, `orientation`, `search_vector` | +| `ControlSystem` | `signage_last_seen`, `playlist_item_id`, `name`, `description`, `version`, `updated_at`, `playlists`, `orientation`, `search_vector` | -The SQL trigger skips CDC rows and notifications when an update changes only ignored fields; the values still persist. Automatic `updated_at` and generated `search_vector` changes are included so ordinary metadata saves stay silent. Changes to other columns still notify, even when the same write changes ignored fields. Inserts and deletes are unchanged. Driver saves also skip saving associated modules whose copied name and role already match, preventing redundant module events; `Driver.module_name` and role changes still synchronize associated modules and emit their events. `Driver.module_name` and `Module.name` are not ignored. +The SQL trigger skips CDC rows and notifications when an update changes only ignored fields; the values still persist. Automatic `updated_at` and generated `search_vector` changes are included so ordinary metadata saves stay silent. Changes to other columns still notify, even when the same write changes ignored fields. Inserts and deletes are unchanged. Driver saves also skip saving associated modules whose copied name and role already match, preventing redundant module events; `Driver.module_name` and role changes still synchronize associated modules and emit their events. `Driver.module_name`, `Module.name` and `ControlSystem.display_name` are not ignored. Setting or clearing ControlSystem display_name emits an update. Policies apply to every writer once the model's changefeed is registered. No-op updates on these tables are also silent. Consumers that need current metadata or signage configuration should read PostgreSQL rather than rely on these changefeeds. `created_at` and other unlisted fields remain notification-producing. Deploy EventBus 1.1.0 or newer to every service that installs CDC triggers before enabling filtering. Older installers can restore the combined trigger and produce unwanted or duplicate events. pg-orm 2.4.1 or newer passes the model declaration, including explicit database-only columns, to EventBus; no core-side filter is required. -Module, Driver and Zone acquire their policies on first registration without a schema migration. For an existing ControlSystem policy, coordinate upgrading its subscribers and explicitly replace the installed policy before they register the new declaration. Old declarations conflict with the new policy, so avoid overlapping registration by the two versions. With the current models loaded, upgrade from the previous eight-column policy using: +Module, Driver and Zone acquire their policies on first registration without a schema migration. For an existing ControlSystem policy, coordinate upgrading its subscribers and explicitly replace the installed policy before they register the new declaration. Old declarations conflict with the new policy, so avoid overlapping registration by the two versions. With the current models loaded, upgrade from the previous ten-column policy using: ```crystal EventBus.new(ENV["PG_DATABASE_URL"]).replace_cdc_update_policy( "sys", ignore_update_columns: PlaceOS::Model::ControlSystem.changefeed_ignored_update_columns.not_nil!, - expected_ignore_update_columns: ["signage_last_seen", "playlist_item_id", "name", "description", "display_name", "version", "updated_at", "search_vector"] + expected_ignore_update_columns: ["signage_last_seen", "playlist_item_id", "name", "description", "display_name", "version", "updated_at", "playlists", "orientation", "search_vector"] ) ``` -If only the original two-column policy was installed, use `["signage_last_seen", "playlist_item_id"]` as the expected list instead. Fresh installations need no replacement. Do not guess the installed policy or suppress a mismatch error. For rollback, use `replace_cdc_update_policy` with the expected current columns; merely removing a declaration preserves the installed policy. +For the earlier eight-column policy, omit `playlists` and `orientation` from the expected list. If only the original two-column policy was installed, use `["signage_last_seen", "playlist_item_id"]` as the expected list instead. Fresh installations need no replacement. Do not guess the installed policy or suppress a mismatch error. For rollback, use `replace_cdc_update_policy` with the expected current columns; merely removing a declaration preserves the installed policy. ## Testing diff --git a/spec/control_system_changefeed_spec.cr b/spec/control_system_changefeed_spec.cr index 9a18b10c..b51cd91b 100644 --- a/spec/control_system_changefeed_spec.cr +++ b/spec/control_system_changefeed_spec.cr @@ -78,7 +78,7 @@ module PlaceOS::Model driver.try &.delete end - {"name", "description", "display_name", "version"}.each do |column| + {"name", "description", "version"}.each do |column| it "persists #{column}-only changes without CDC rows or notifications" do system = Generator.control_system.save! id = system.id.as(String) @@ -95,10 +95,9 @@ module PlaceOS::Model value = column == "version" ? "7" : "metadata-#{RANDOM.hex(8)}" previous_updated_at = system.updated_at case column - when "name" then system.name = value - when "description" then system.description = value - when "display_name" then system.display_name = value - when "version" then system.version = value.to_i + when "name" then system.name = value + when "description" then system.description = value + when "version" then system.version = value.to_i end system.save! system.reload! @@ -106,12 +105,6 @@ module PlaceOS::Model PgORM::Database.connection do |db| db.query_one("SELECT #{column}::text FROM sys WHERE id = $1", args: [id], as: String).should eq(value) end - if column == "display_name" - system.display_name = nil - system.save! - system.reload! - system.display_name.should be_nil - end signage_cdc_actions(id).should eq(before) PgORM::Database.connection { |db| db.exec("SELECT pg_notify('metadata_spec_barrier', $1)", args: [barrier]) } receive_signage_notification(notifications).should eq(barrier) @@ -122,6 +115,34 @@ module PlaceOS::Model end end + it "notifies when display_name is set or cleared alongside ignored metadata" do + system = Generator.control_system.save! + id = system.id.as(String) + feed = ControlSystem.changes + notifications = Channel(String).new(4) + listener = PG::ListenConnection.new(ENV["PG_DATABASE_URL"], ["cdc_events"]) do |notification| + payload = JSON.parse(notification.payload) + if payload["table"].as_s == "sys" && payload["id"].as_s == id + notifications.send(notification.payload) + end + end + expected_actions = signage_cdc_actions(id) + {"Updated display", nil}.each do |display_name| + system.display_name = display_name + system.description = "metadata with #{display_name || "cleared display"}" + system.save! + system.reload! + system.display_name.should eq(display_name) + expected_actions << "update" + signage_cdc_actions(id).should eq(expected_actions) + JSON.parse(receive_signage_notification(notifications))["action"].as_s.should eq("update") + end + ensure + listener.try &.close + feed.try &.stop + system.try &.delete + end + it "keeps runtime Zone update notifications" do zone = Generator.zone.save! notifications = Channel(String).new(4) diff --git a/src/placeos-models/control_system.cr b/src/placeos-models/control_system.cr index 4ec4d647..f982cb48 100644 --- a/src/placeos-models/control_system.cr +++ b/src/placeos-models/control_system.cr @@ -76,7 +76,7 @@ module PlaceOS::Model # Telemetry and descriptive metadata do not require running drivers to reload. # ORM saves advance updated_at; PostgreSQL also regenerates search_vector. changefeed_ignore_updates :signage_last_seen, :playlist_item_id, - :name, :description, :display_name, :version, :playlists, :orientation, :updated_at, + :name, :description, :version, :playlists, :orientation, :updated_at, database_columns: [:search_vector] attribute space_config : Hash(String, JSON::Any) = {} of String => JSON::Any diff --git a/tasks/146-display-name.md b/tasks/146-display-name.md new file mode 100644 index 00000000..8dfa9a02 --- /dev/null +++ b/tasks/146-display-name.md @@ -0,0 +1,11 @@ +# Restore ControlSystem display_name events + +Issue: https://github.com/PlaceOS/local/issues/146 +Owner: codex-146-display-name-20260916-root + +- [x] Regress setting and clearing display_name via save! emits PG notifications and CDC rows. +- [x] Remove only ControlSystem display_name exclusion; retain remaining policies and suppression regressions. +- [x] Update documented installed-policy replacement from previous ten-column policy. +- [ ] Focused red/green, lint/format, full stable/unstable CI and review, then squash merge. + +Regression red: 6 specs, one failure, expected update CDC row absent on display_name save. Removed only the ControlSystem exclusion. Formatting/Ameba pass (181 files); focused green and full CI required before merge. Docs/spec review approved.