From 38c112a6a10f19628ab6cbd5ec6bc8a65359e979 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 23 Apr 2026 14:02:41 -0700 Subject: [PATCH 1/4] improve trim excess sensor reading performance --- app/models/device.rb | 12 ++++++--- spec/models/device_spec.rb | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/models/device.rb b/app/models/device.rb index a7c0873b3..542da2249 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -124,15 +124,19 @@ def trim_excess_telemetry # Give the user back the amount of sensor readings they are allowed to view. def limited_sensor_readings_list sensor_readings - .order(created_at: :desc) + .order(created_at: :desc, id: :desc) .limit(DEFAULT_MAX_SENSOR_READINGS) end def excess_sensor_readings + sensor_readings.where(id: excess_sensor_reading_ids_subquery) + end + + def excess_sensor_reading_ids_subquery sensor_readings - .where - .not(id: limited_sensor_readings_list.pluck(:id)) - .where(device_id: self.id) + .order(created_at: :desc, id: :desc) + .offset(DEFAULT_MAX_SENSOR_READINGS) + .select(:id) end def trim_excess_sensor_readings diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb index 34ed04bfd..1d6502bcf 100644 --- a/spec/models/device_spec.rb +++ b/spec/models/device_spec.rb @@ -117,6 +117,58 @@ device.send_upgrade_request end + it "builds a DB-only subquery for excess sensor readings" do + relation = device.excess_sensor_readings + + expect(relation.to_sql).to include("OFFSET #{Device::DEFAULT_MAX_SENSOR_READINGS}") + expect(relation.to_sql).to include("\"sensor_readings\".\"id\" IN") + end + + it "returns limited sensor readings in reverse chronological order" do + const_reassign(Device, :DEFAULT_MAX_SENSOR_READINGS, 2) do + oldest = FactoryBot.create(:sensor_reading, + device: device, + created_at: 2.seconds.ago, + updated_at: 2.seconds.ago) + tied_time = 1.second.ago + older_tied = FactoryBot.create(:sensor_reading, + device: device, + created_at: tied_time, + updated_at: tied_time) + newer_tied = FactoryBot.create(:sensor_reading, + device: device, + created_at: tied_time, + updated_at: tied_time) + + expect(device.limited_sensor_readings_list.pluck(:id)) + .to eq([newer_tied.id, older_tied.id]) + expect(device.limited_sensor_readings_list.pluck(:id)) + .not_to include(oldest.id) + end + end + + it "trims older sensor readings beyond the device limit" do + const_reassign(Device, :DEFAULT_MAX_SENSOR_READINGS, 2) do + oldest = FactoryBot.create(:sensor_reading, + device: device, + created_at: 3.seconds.ago, + updated_at: 3.seconds.ago) + middle = FactoryBot.create(:sensor_reading, + device: device, + created_at: 2.seconds.ago, + updated_at: 2.seconds.ago) + newest = FactoryBot.create(:sensor_reading, + device: device, + created_at: 1.second.ago, + updated_at: 1.second.ago) + + device.trim_excess_sensor_readings + + expect(device.sensor_readings.pluck(:id)).to match_array([middle.id, newest.id]) + expect(SensorReading.exists?(oldest.id)).to be(false) + end + end + it "reports unknown location in feedback payload when coordinates are missing" do expect(Faraday).to receive(:post) do |_url, payload, _headers| text = JSON.parse(payload)["text"] From 7682e86d2859c2d2214a32136dd65fc2457d8391 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 23 Apr 2026 14:31:10 -0700 Subject: [PATCH 2/4] fix sequence panel content visibility issue --- frontend/css/panels/sequences.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/css/panels/sequences.scss b/frontend/css/panels/sequences.scss index f978dad97..ea93d3266 100644 --- a/frontend/css/panels/sequences.scss +++ b/frontend/css/panels/sequences.scss @@ -36,7 +36,8 @@ } .panel-content { .sequence-editor-content { - overflow: hidden; + overflow-x: hidden; + overflow-y: visible; @media screen and (max-width: 767px) { margin-left: 5px; From 7a2b1417683a1e285c0c711d4d575bb63a86292c Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 23 Apr 2026 15:43:29 -0700 Subject: [PATCH 3/4] add passenger max-requests param --- Procfile | 2 +- docker-compose.yml | 2 +- example.env | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Procfile b/Procfile index 76f9d192d..9a78e97e1 100644 --- a/Procfile +++ b/Procfile @@ -1,5 +1,5 @@ worker: bundle exec rake jobs:work rabbit_workers: bin/rails r lib/rabbit_workers.rb -web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size $MAX_POOL_SIZE +web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size ${MAX_POOL_SIZE:-1} --max-requests ${MAX_REQUESTS:-1000} # This will perform a hard refresh on all connected browsers. release: rails r "User.refresh_everyones_ui" && rails db:migrate && (bundle exec rake hook:release_info || true) diff --git a/docker-compose.yml b/docker-compose.yml index e84faf8b8..19e018111 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,7 @@ services: build: context: "." dockerfile: docker_configs/api.Dockerfile - command: bash -c "rm -f tmp/pids/server.pid && bundle exec passenger start" + command: bash -c "rm -f tmp/pids/server.pid && bundle exec passenger start --max-pool-size ${MAX_POOL_SIZE:-1} --max-requests ${MAX_REQUESTS:-1000}" ports: ["${API_PORT}:${API_PORT}"] mqtt: diff --git a/example.env b/example.env index a42c94a50..995dca681 100644 --- a/example.env +++ b/example.env @@ -142,7 +142,12 @@ CODECOV_TOKEN= # Set the max pool size for Passenger. (Only needed if using Heroku) # FarmBot Inc uses Heroku. Self hosters do not. -MAX_POOL_SIZE=2 +MAX_POOL_SIZE=1 + +# Set the max number of requests until restart for Passenger. +# (Only needed if using Heroku) +# FarmBot Inc uses Heroku. Self hosters do not. +MAX_REQUESTS=1000 # This is set by Heroku and used by the frontend to show the current version. # Most self hosting users will want to delete this. From 9ff9d7795e98a14819202ab9c8629f1ed709c244 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Thu, 23 Apr 2026 17:51:09 -0700 Subject: [PATCH 4/4] add command fix --- frontend/css/panels/sequences.scss | 59 ++++++++----------- .../sequence_editor_middle_active_test.tsx | 14 ++--- frontend/sequences/all_steps.tsx | 4 +- .../sequence_editor_middle_active.tsx | 22 +++---- 4 files changed, 44 insertions(+), 55 deletions(-) diff --git a/frontend/css/panels/sequences.scss b/frontend/css/panels/sequences.scss index ea93d3266..12b7be3c0 100644 --- a/frontend/css/panels/sequences.scss +++ b/frontend/css/panels/sequences.scss @@ -45,14 +45,9 @@ } } .add-command-button-container { - display: block; - height: 0; - .bp6-collapse { - padding: 0; - } - .bp6-collapse-body { - padding: 0; - } + align-items: start; + justify-content: right; + grid-template-columns: 1fr auto; } .drag-drop-area { display: none; @@ -259,7 +254,6 @@ .sequence-editor-sections { .bp6-collapse { - padding: 0 1rem; .bp6-collapse-body { padding-bottom: 1rem; } @@ -552,8 +546,11 @@ } } +.sequence-step-components { + padding: 0 1rem; +} + .sequence-steps { - margin-right: 2.5rem; .sequence-step { &.hovered { box-shadow: 0px 0px 15px #f70; @@ -572,10 +569,20 @@ } } +.sequence-steps > .step-dragger { + margin-right: 2.5rem; +} + +.sequence-page { + .step-dragger { + margin-right: 0; + } + .add-command-button-container { + display: none; + } +} + .step-button-cluster { - overflow-y: auto; - overflow-x: hidden; - max-height: calc(100vh - 8rem); .text-input-wrapper { input { padding: 0; @@ -608,7 +615,6 @@ &.designer-cluster { margin: 0; background: $dark_gray; - margin-bottom: 1rem; border-radius: 5px; overflow: unset; padding: 0.75rem; @@ -1088,11 +1094,9 @@ } .add-command-button-container { - display: none; - position: absolute; - z-index: 9; - width: 100%; - height: 0; + .bp6-collapse { + transition-duration: 0ms !important; + } button { margin: 0; } @@ -1104,6 +1108,8 @@ height: 20px; padding: 0.25rem; border-radius: 2rem; + margin-bottom: -.75rem; + right: -2.5rem; box-shadow: none !important; background: $dark_gray !important; i { @@ -1131,7 +1137,6 @@ &.last { .bp6-collapse { margin-top: 1rem; - margin-right: 2.5rem; } .add-command { margin-top: -2rem; @@ -1146,10 +1151,6 @@ } } &.only { - position: relative; - margin: auto; - text-align: center; - height: 3rem; .add-command { display: none; } @@ -1157,18 +1158,8 @@ margin: 0; } } - - @media screen and (max-width: 767px) { - display: block; - .add-command { - display: block; - } - } &.open { - position: relative; .add-command { - position: absolute; - right: -2.5rem; transform: rotate(-45deg); i { transform: rotate(0); diff --git a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx index 6ff8caae7..0549f72f1 100644 --- a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx +++ b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx @@ -8,6 +8,7 @@ import { SequenceBtnGroup, SequenceShareMenu, SequencePublishMenu, + AddCommandButton, isSequencePublished, AddCommandButtonProps, } from "../sequence_editor_middle_active"; @@ -835,11 +836,6 @@ describe("", () => { stepButtonClusterSpy.mockRestore(); }); - const getAddCommandButton = async () => { - return (await import(`../sequence_editor_middle_active.tsx?m=${Math.random()}`)) - .AddCommandButton; - }; - const fakeProps = (): AddCommandButtonProps => ({ dispatch: jest.fn(), index: 1, @@ -850,10 +846,9 @@ describe("", () => { resources: buildResourceIndex().index, }); - it("dispatches new step position", async () => { + it("dispatches new step position", () => { location.pathname = ""; const p = fakeProps(); - const AddCommandButton = await getAddCommandButton(); const wrapper = createRenderer(); const button = wrapper.root.findAll(node => typeof node.props.onClick == "function" && @@ -876,12 +871,13 @@ describe("", () => { unmountRenderer(wrapper); }); - it("closes cluster", async () => { - const AddCommandButton = await getAddCommandButton(); + it("closes cluster", () => { const { container } = render(); const cluster = container.querySelector(".add-command-button-container"); const close = container.querySelector(".step-button-cluster-close"); if (cluster && close) { + expect(cluster.classList.contains("row")).toBeTruthy(); + expect(cluster.classList.contains("half-gap")).toBeTruthy(); expect(cluster.classList.contains("open")).toBeTruthy(); fireEvent.click(close); expect(cluster.classList.contains("open")).toBeFalsy(); diff --git a/frontend/sequences/all_steps.tsx b/frontend/sequences/all_steps.tsx index af0a7fdf2..27aa12043 100644 --- a/frontend/sequences/all_steps.tsx +++ b/frontend/sequences/all_steps.tsx @@ -55,15 +55,15 @@ export class AllSteps extends React.Component { const hovered = this.props.visualized && this.props.hoveredStep === tag ? "hovered" : ""; - return
+ this.props.onDrop(index, key)} /> {!this.props.readOnly && } - this.props.onDrop(index, key)} /> { "add-command-button-container", getPositionClass(), collapsed ? "" : "open", + "row", + "half-gap", ].join(" ")}> + + setCollapsed(true)} + current={props.sequence} + dispatch={dispatch} + farmwareData={props.farmwareData} + sequences={props.sequences} + resources={props.resources} + stepIndex={index} /> + - - setCollapsed(true)} - current={props.sequence} - dispatch={dispatch} - farmwareData={props.farmwareData} - sequences={props.sequences} - resources={props.resources} - stepIndex={index} /> -
; };