From 20936186366acc2425cb7eec1540e4a253ed40dc Mon Sep 17 00:00:00 2001 From: dariuszprzepiora <34426341+dariuszprzepiora@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:09:44 +0200 Subject: [PATCH 1/3] fix(mqtt): keep credentials and TLS in ConnectionSettings php-mqtt/client ConnectionSettings is immutable: every setter returns a modified clone. buildSettings() discarded those return values, so the username, password and TLS options never reached the connection. Brokers with anonymous access disabled rejected the listener as "not authorised", with nothing pointing at the missing credentials. Co-Authored-By: Claude Opus 5.5 --- backend/app/Console/Commands/MqttListenCommand.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/Console/Commands/MqttListenCommand.php b/backend/app/Console/Commands/MqttListenCommand.php index 93995b441..abb655f9d 100644 --- a/backend/app/Console/Commands/MqttListenCommand.php +++ b/backend/app/Console/Commands/MqttListenCommand.php @@ -245,20 +245,20 @@ private function buildSettings(mixed $cfg): ConnectionSettings ->setReconnectAutomatically(false); if ($cfg->username) { - $settings->setUsername($cfg->username); + $settings = $settings->setUsername($cfg->username); } $password = $cfg->getPassword(); if ($password) { - $settings->setPassword($password); + $settings = $settings->setPassword($password); } if ($cfg->use_tls) { - $settings->setUseTls(true); + $settings = $settings->setUseTls(true); if ($cfg->ca_cert) { $caFile = tempnam(sys_get_temp_dir(), 'mqtt_ca_'); file_put_contents($caFile, $cfg->ca_cert); - $settings->setTlsCertificateAuthorityFile($caFile); + $settings = $settings->setTlsCertificateAuthorityFile($caFile); } } From 78fc55bd80d418b17fc2292a9261dc95da7ea3f6 Mon Sep 17 00:00:00 2001 From: dariuszprzepiora <34426341+dariuszprzepiora@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:09:46 +0200 Subject: [PATCH 2/3] fix(docker): broadcast live-sync deltas from mqtt-listener mqtt-listener was pinned to BROADCAST_CONNECTION=log. CollectionChanged is ShouldBroadcastNow, so it is dispatched by the process that writes the row; work order quantities updated from MQTT messages were logged instead of sent to Reverb, and open pages only showed them after a manual reload. Use reverb with the same REVERB_* settings as backend and queue-worker. Co-Authored-By: Claude Opus 5.5 --- docker-compose.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4ecac4cf9..2c5776ed9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -195,7 +195,15 @@ services: DB_DATABASE: ${POSTGRES_DB:-openmmes} DB_USERNAME: ${POSTGRES_USER:-openmmes_user} DB_PASSWORD: ${POSTGRES_PASSWORD:-openmmes_secret} - BROADCAST_CONNECTION: log + # Live sync: CollectionChanged is ShouldBroadcastNow, so it is sent by the + # process that writes the row — here, machine counts arriving over MQTT. + BROADCAST_CONNECTION: reverb + REVERB_APP_ID: ${REVERB_APP_ID:-openmes} + REVERB_APP_KEY: ${REVERB_APP_KEY:-openmeskey} + REVERB_APP_SECRET: ${REVERB_APP_SECRET:-openmessecret} + REVERB_HOST: ${REVERB_HOST:-reverb} + REVERB_PORT: ${REVERB_PORT:-8080} + REVERB_SCHEME: ${REVERB_SCHEME:-http} depends_on: postgres: condition: service_healthy From 00dea4c5c987942963c2263a8cd455bdd25c5ad4 Mon Sep 17 00:00:00 2001 From: jakub-przepiora Date: Sat, 3 Oct 2026 13:46:26 +0200 Subject: [PATCH 3/3] fix(docker): broadcast live-sync deltas from every process that writes rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mqtt-listener was not the only service pinned away from Reverb. modbus-poller and queue-worker set no BROADCAST_CONNECTION at all, so they fell back to the config default — reverb — with no REVERB_HOST, REVERB_PORT or REVERB_SCHEME. That resolves to https://:443, which goes nowhere. The failure is invisible by design: CollectionBroadcaster::safeBroadcast() reports the exception and swallows it, because a broadcast must never break the write that triggered it. So the counter reading lands in the database and the browser keeps showing the old number until someone reloads. modbus-poller writes machine counter readings and queue-worker sends every queued ShouldBroadcast event, so both need the same block the listener now has. opcua-gateway is unaffected: it posts to the backend API, and the backend both writes the row and broadcasts it. Also adds a unit test for MqttListenCommand::buildSettings(). ConnectionSettings is immutable, so a setter whose result is discarded is a no-op that nothing reports; two of the four assertions fail without the credentials fix in this branch. --- .../MqttConnectionSettingsTest.php | 93 +++++++++++++++++++ docker-compose.yml | 20 ++++ 2 files changed, 113 insertions(+) create mode 100644 backend/tests/Unit/Connectivity/MqttConnectionSettingsTest.php diff --git a/backend/tests/Unit/Connectivity/MqttConnectionSettingsTest.php b/backend/tests/Unit/Connectivity/MqttConnectionSettingsTest.php new file mode 100644 index 000000000..d68335126 --- /dev/null +++ b/backend/tests/Unit/Connectivity/MqttConnectionSettingsTest.php @@ -0,0 +1,93 @@ +setAccessible(true); + + return $method->invoke(app(MqttListenCommand::class), $cfg); + } + + private function connection(array $attributes = []): MqttConnection + { + $cfg = new MqttConnection(array_merge([ + 'broker_host' => 'broker.local', + 'broker_port' => 1883, + 'keep_alive_seconds' => 60, + 'connect_timeout' => 5, + ], $attributes)); + + $cfg->machine_connection_id = 1; + + return $cfg; + } + + public function test_a_username_and_password_reach_the_connection(): void + { + $cfg = $this->connection(['username' => 'openmes']); + $cfg->password = 'secret'; + + $settings = $this->buildSettings($cfg); + + $this->assertSame('openmes', $settings->getUsername()); + $this->assertSame('secret', $settings->getPassword()); + } + + public function test_an_anonymous_broker_is_left_without_credentials(): void + { + $settings = $this->buildSettings($this->connection()); + + $this->assertNull($settings->getUsername()); + $this->assertNull($settings->getPassword()); + } + + public function test_tls_and_its_certificate_authority_reach_the_connection(): void + { + $cfg = $this->connection([ + 'use_tls' => true, + 'ca_cert' => "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", + ]); + + $settings = $this->buildSettings($cfg); + + $this->assertTrue($settings->shouldUseTls()); + + $caFile = $settings->getTlsCertificateAuthorityFile(); + $this->assertNotNull($caFile); + $this->assertStringContainsString('BEGIN CERTIFICATE', file_get_contents($caFile)); + + @unlink($caFile); + } + + public function test_the_timings_saved_on_the_device_are_kept(): void + { + $settings = $this->buildSettings($this->connection([ + 'keep_alive_seconds' => 15, + 'connect_timeout' => 9, + ])); + + $this->assertSame(15, $settings->getKeepAliveInterval()); + $this->assertSame(9, $settings->getConnectTimeout()); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 2c5776ed9..941cbaf7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -251,6 +251,17 @@ services: DB_DATABASE: ${POSTGRES_DB:-openmmes} DB_USERNAME: ${POSTGRES_USER:-openmmes_user} DB_PASSWORD: ${POSTGRES_PASSWORD:-openmmes_secret} + # Live sync: same reason as mqtt-listener — this process writes the + # counter readings, so it is the one that has to reach Reverb. Without + # these the default connection is still reverb, but with no host it + # dials https://:443 and the delta is silently dropped. + BROADCAST_CONNECTION: reverb + REVERB_APP_ID: ${REVERB_APP_ID:-openmes} + REVERB_APP_KEY: ${REVERB_APP_KEY:-openmeskey} + REVERB_APP_SECRET: ${REVERB_APP_SECRET:-openmessecret} + REVERB_HOST: ${REVERB_HOST:-reverb} + REVERB_PORT: ${REVERB_PORT:-8080} + REVERB_SCHEME: ${REVERB_SCHEME:-http} depends_on: postgres: condition: service_healthy @@ -322,6 +333,15 @@ services: DB_DATABASE: ${POSTGRES_DB:-openmmes} DB_USERNAME: ${POSTGRES_USER:-openmmes_user} DB_PASSWORD: ${POSTGRES_PASSWORD:-openmmes_secret} + # Live sync: queued ShouldBroadcast events are sent from this process, + # not from backend. Same defaulting trap as the listeners above. + BROADCAST_CONNECTION: reverb + REVERB_APP_ID: ${REVERB_APP_ID:-openmes} + REVERB_APP_KEY: ${REVERB_APP_KEY:-openmeskey} + REVERB_APP_SECRET: ${REVERB_APP_SECRET:-openmessecret} + REVERB_HOST: ${REVERB_HOST:-reverb} + REVERB_PORT: ${REVERB_PORT:-8080} + REVERB_SCHEME: ${REVERB_SCHEME:-http} depends_on: postgres: condition: service_healthy