Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 32 additions & 11 deletions spec/control_system_changefeed_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -95,23 +95,16 @@ 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!
system.updated_at.should be > previous_updated_at
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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/placeos-models/control_system.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tasks/146-display-name.md
Original file line number Diff line number Diff line change
@@ -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.
Loading