diff --git a/Gemfile.lock b/Gemfile.lock
index 422fbcd4d9..6bd053f870 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -119,7 +119,7 @@ GEM
railties (>= 5.0.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
- faraday (2.12.2)
+ faraday (2.13.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -188,7 +188,7 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
- logger (1.6.6)
+ logger (1.7.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -222,18 +222,18 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.18.6-aarch64-linux-gnu)
+ nokogiri (1.18.7-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.18.6-x86_64-linux-gnu)
+ nokogiri (1.18.7-x86_64-linux-gnu)
racc (~> 1.4)
orm_adapter (0.5.0)
os (1.1.4)
- parser (3.3.7.3)
+ parser (3.3.7.4)
ast (~> 2.4.1)
racc
- passenger (6.0.23)
+ passenger (6.0.27)
rack (>= 1.6.13)
- rackup
+ rackup (>= 1.0.1)
rake (>= 12.3.3)
pg (1.5.9)
pry (0.15.2)
@@ -306,7 +306,7 @@ GEM
railties (>= 5.2)
retriable (3.1.2)
rexml (3.4.1)
- rollbar (3.6.1)
+ rollbar (3.6.2)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
diff --git a/app/controllers/api/logs_controller.rb b/app/controllers/api/logs_controller.rb
index a04e64edc5..86d1973ace 100644
--- a/app/controllers/api/logs_controller.rb
+++ b/app/controllers/api/logs_controller.rb
@@ -14,19 +14,27 @@ class LogsController < Api::AbstractController
def search
conf = current_device.web_app_config
mt = CeleryScriptSettingsBag::ALLOWED_MESSAGE_TYPES
- query = mt
- .map { |x| "(type = '#{x}' AND verbosity <= ?)" }
- .join(" OR ")
- conditions = mt.map { |x| "#{x}_log" }.map { |x| conf.send(x) }
- args_ = conditions.unshift(query)
limit = current_device.max_log_count || Device::DEFAULT_MAX_LOGS
- render json: current_device
- .logs
- .order(created_at: :desc)
- .where(*args_)
- .limit(limit)
- .where(search_params)
+ # Build conditions once
+ type_conditions = mt.map.with_index do |type, i|
+ verbosity = conf.send("#{type}_log")
+ ["(type = ? AND verbosity <= ?)", type, verbosity]
+ end
+
+ # Combine conditions efficiently
+ where_clause = type_conditions.map(&:first).join(" OR ")
+ where_values = type_conditions.flat_map { |c| c[1..-1] }
+
+ # Single query with all conditions
+ logs = current_device
+ .logs
+ .order(created_at: :desc)
+ .limit(limit)
+ .where(where_clause, *where_values)
+ .where(search_params)
+
+ render json: logs
end
def create
diff --git a/app/controllers/api/sensor_readings_controller.rb b/app/controllers/api/sensor_readings_controller.rb
index cd1f8b9b48..7e6f050e40 100644
--- a/app/controllers/api/sensor_readings_controller.rb
+++ b/app/controllers/api/sensor_readings_controller.rb
@@ -1,6 +1,5 @@
module Api
class SensorReadingsController < Api::AbstractController
- LIMIT = 2500
before_action :clean_old
def create
@@ -23,20 +22,14 @@ def destroy
private
def clean_old
- if current_device.sensor_readings.count > LIMIT
- current_device
- .sensor_readings
- .where
- .not(id: readings.pluck(:id))
- .delete_all
- end
+ current_device.delay.trim_excess_sensor_readings
end
def readings
@readings ||= SensorReading
.where(device: current_device)
.order(created_at: :desc)
- .limit(LIMIT)
+ .limit(Device::DEFAULT_MAX_SENSOR_READINGS)
end
def reading
diff --git a/app/controllers/api/sequences_controller.rb b/app/controllers/api/sequences_controller.rb
index 640a82a0fb..bb147cbadd 100644
--- a/app/controllers/api/sequences_controller.rb
+++ b/app/controllers/api/sequences_controller.rb
@@ -1,11 +1,28 @@
module Api
class SequencesController < Api::AbstractController
+ include ActionController::Live
+
before_action :clean_expired_farm_events, only: [:destroy]
def index
- render json: sequences
- .to_a
- .map { |s| Sequences::Show.run!(sequence: s) }
+ # Stream to reduce memory usage
+ response.headers['Content-Type'] = 'application/json'
+ response.headers['Cache-Control'] = 'no-cache'
+ response.stream.write '['
+
+ Sequence.where(device: current_device)
+ .includes(:sequence_publication, :sequence_version)
+ .each_with_index do |s, index|
+ # Load the sequence with all needed associations and convert to JSON
+ seq_json = Sequences::Show.run!(sequence: Sequence.with_usage_reports.find(s.id)).to_json
+ # Append a comma for all but the first element to maintain valid JSON syntax
+ response.stream.write ',' unless index.zero?
+ response.stream.write seq_json
+ end
+
+ response.stream.write ']'
+ ensure
+ response.stream.close
end
def show
@@ -65,12 +82,9 @@ def sequence_params
@sequence_params ||= raw_json[:sequence] || raw_json || {}
end
- def sequences
- @sequences ||= Sequence.with_usage_reports.where(device: current_device)
- end
-
+ # Retrieve a single sequence record directly associated with the current device
def sequence
- @sequence ||= sequences.find(params[:id])
+ @sequence ||= Sequence.with_usage_reports.find_by!(id: params[:id], device: current_device)
end
end
end
diff --git a/app/models/device.rb b/app/models/device.rb
index a7830f3e70..05f708f262 100644
--- a/app/models/device.rb
+++ b/app/models/device.rb
@@ -4,6 +4,7 @@ class Device < ApplicationRecord
DEFAULT_MAX_IMAGES = 100
DEFAULT_MAX_LOGS = 1000
DEFAULT_MAX_TELEMETRY = 300
+ DEFAULT_MAX_SENSOR_READINGS = 2500
DEFAULT_MAX_LOG_AGE_IN_DAYS = 60
DEFAULT_MAX_SEQUENCE_COUNT = 75
DEFAULT_MAX_SEQUENCE_LENGTH = 30
@@ -120,6 +121,24 @@ def trim_excess_telemetry
excess_telemetry.delete_all
end
+ # 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)
+ .limit(DEFAULT_MAX_SENSOR_READINGS)
+ end
+
+ def excess_sensor_readings
+ sensor_readings
+ .where
+ .not(id: limited_sensor_readings_list.pluck(:id))
+ .where(device_id: self.id)
+ end
+
+ def trim_excess_sensor_readings
+ excess_sensor_readings.delete_all
+ end
+
def self.current
RequestStore.store[:device]
end
diff --git a/app/models/sequence.rb b/app/models/sequence.rb
index 70fc89a7d2..188b957b3b 100644
--- a/app/models/sequence.rb
+++ b/app/models/sequence.rb
@@ -23,6 +23,8 @@ class Sequence < ApplicationRecord
has_many :regimen_items
has_many :primary_nodes, dependent: :destroy
has_many :edge_nodes, dependent: :destroy
+ has_one :sequence_publication, foreign_key: :author_sequence_id
+ belongs_to :sequence_version, optional: true
# allowable label colors for the frontend.
[:name, :kind].each { |n| validates n, presence: true }
diff --git a/app/mutations/sequences/show.rb b/app/mutations/sequences/show.rb
index 20df4c6d7b..be8c22c5b7 100644
--- a/app/mutations/sequences/show.rb
+++ b/app/mutations/sequences/show.rb
@@ -63,7 +63,11 @@ def copyright
end
def sequence_publication
- @sequence_publication ||= SequencePublication.find_by(author_sequence_id: sequence.id)
+ # Cache the association if it's already loaded
+ return @sequence_publication if defined?(@sequence_publication)
+ @sequence_publication = sequence.association(:sequence_publication).loaded? ?
+ sequence.sequence_publication :
+ SequencePublication.find_by(author_sequence_id: sequence.id)
end
def description
@@ -71,7 +75,11 @@ def description
end
def sequence_version
- @sequence_version ||= SequenceVersion.find_by(id: sequence_version_id)
+ # Cache the association if it's already loaded
+ return @sequence_version if defined?(@sequence_version)
+ @sequence_version = sequence.association(:sequence_version).loaded? ?
+ sequence.sequence_version :
+ SequenceVersion.find_by(id: sequence_version_id)
end
def use_upstream_version?
@@ -90,7 +98,7 @@ def available_version_ids
# If it is not published, don't show anything to the author.
# If it IS published, show the versions to the author.
if is_owner?
- return sequence_publication.sequence_versions.pluck(:id)
+ return sequence_publication&.sequence_versions&.pluck(:id) || []
end
# Second attempt:
diff --git a/frontend/__test_support__/additional_mocks.tsx b/frontend/__test_support__/additional_mocks.tsx
index 43577a2752..0b0bd69bea 100644
--- a/frontend/__test_support__/additional_mocks.tsx
+++ b/frontend/__test_support__/additional_mocks.tsx
@@ -47,6 +47,7 @@ jest.mock("react-router", () => ({
Route: jest.fn(({ children }) =>
{children}
),
Routes: jest.fn(({ children }) => {children}
),
useNavigate: () => mockNavigate,
+ useLocation: () => window.location,
Navigate: ({ to }: { to: string }) => {mockNavigate(to)}
,
Outlet: jest.fn(() => ),
}));
diff --git a/frontend/controls/move/__tests__/jog_buttons_test.tsx b/frontend/controls/move/__tests__/jog_buttons_test.tsx
index 628993088c..231b28f873 100644
--- a/frontend/controls/move/__tests__/jog_buttons_test.tsx
+++ b/frontend/controls/move/__tests__/jog_buttons_test.tsx
@@ -95,6 +95,7 @@ describe("", () => {
const fakeProps = (): PowerAndResetMenuProps => ({
botOnline: true,
showAdvanced: true,
+ dispatch: jest.fn(),
});
it("restarts firmware", () => {
diff --git a/frontend/controls/move/bot_position_rows.tsx b/frontend/controls/move/bot_position_rows.tsx
index d3a4d07e24..4e4f9e8387 100644
--- a/frontend/controls/move/bot_position_rows.tsx
+++ b/frontend/controls/move/bot_position_rows.tsx
@@ -27,6 +27,7 @@ import {
import { NumberConfigKey } from "farmbot/dist/resources/configs/firmware";
import { isUndefined } from "lodash";
import { calculateScale } from "../../settings/hardware_settings";
+import { setPanelOpen } from "../../farm_designer/panel_header";
export const BotPositionRows = (props: BotPositionRowsProps) => {
const { locationData, getConfigValue, arduinoBusy, locked } = props;
@@ -141,7 +142,10 @@ export const AxisActions = (props: AxisActionsProps) => {
onClick={setAxisLength({ axis, dispatch, botPosition, sourceFwConfig })}>
{t("SET LENGTH")}
- { navigate(Path.settings("axes")); }}>
+ {
+ dispatch(setPanelOpen(true));
+ navigate(Path.settings("axes"));
+ }}>
{t("Settings")}
diff --git a/frontend/controls/move/jog_buttons.tsx b/frontend/controls/move/jog_buttons.tsx
index cc16ff2726..3e9f3cb2d8 100644
--- a/frontend/controls/move/jog_buttons.tsx
+++ b/frontend/controls/move/jog_buttons.tsx
@@ -97,7 +97,9 @@ export class JogButtons
target={}
- content={} />
@@ -160,6 +162,7 @@ export class JogButtons
export interface PowerAndResetMenuProps {
botOnline: boolean;
showAdvanced: boolean;
+ dispatch: Function;
}
export const PowerAndResetMenu = (props: PowerAndResetMenuProps) => {
@@ -188,6 +191,8 @@ export const PowerAndResetMenu = (props: PowerAndResetMenuProps) => {
buttonText={t("SHUTDOWN")}
color={"red"}
action={powerOff} />
-
+
;
};
diff --git a/frontend/devices/connectivity/__tests__/diagnosis_test.tsx b/frontend/devices/connectivity/__tests__/diagnosis_test.tsx
index 5c4bbfb889..6e713c4e8b 100644
--- a/frontend/devices/connectivity/__tests__/diagnosis_test.tsx
+++ b/frontend/devices/connectivity/__tests__/diagnosis_test.tsx
@@ -17,6 +17,7 @@ describe("", () => {
botAPI: true,
botFirmware: true,
},
+ dispatch: jest.fn(),
});
it("renders help text", () => {
diff --git a/frontend/devices/connectivity/connectivity.tsx b/frontend/devices/connectivity/connectivity.tsx
index 592bbf31ff..32ae7f5d59 100644
--- a/frontend/devices/connectivity/connectivity.tsx
+++ b/frontend/devices/connectivity/connectivity.tsx
@@ -114,7 +114,7 @@ export class Connectivity
: undefined}
hover={this.hover}
hoveredConnection={this.state.hoveredConnection} />)}
-
+
{this.props.flags.userAPI && this.props.flags.userMQTT
&& this.props.flags.botAPI && this.props.flags.botMQTT
&& this.props.apiFirmwareValue
@@ -162,7 +162,11 @@ export class Connectivity
-
+
{t("Learn more about ports")}
diff --git a/frontend/devices/connectivity/diagnosis.tsx b/frontend/devices/connectivity/diagnosis.tsx
index ecc9343aa2..2fba974667 100644
--- a/frontend/devices/connectivity/diagnosis.tsx
+++ b/frontend/devices/connectivity/diagnosis.tsx
@@ -8,6 +8,7 @@ import { SyncStatus } from "farmbot";
import { syncText } from "../../nav/sync_text";
import { useNavigate } from "react-router";
import { linkToSetting } from "../../settings/maybe_highlight";
+import { setPanelOpen } from "../../farm_designer/panel_header";
export type ConnectionName =
| "userAPI"
@@ -20,6 +21,7 @@ export type ConnectionStatusFlags = Record;
export interface DiagnosisProps {
statusFlags: ConnectionStatusFlags;
hideGraphic?: boolean;
+ dispatch: Function;
}
export interface DiagnosisSaucerProps extends ConnectionStatusFlags {
className?: string;
@@ -61,7 +63,10 @@ export function Diagnosis(props: DiagnosisProps) {
{t("Always")}
{ navigate(linkToSetting(DeviceSetting.farmbotOS)); }}>
+ onClick={() => {
+ props.dispatch(setPanelOpen(true));
+ navigate(linkToSetting(DeviceSetting.farmbotOS));
+ }}>
{t("upgrade FarmBot OS")}
{t("before troubleshooting.")}
@@ -69,11 +74,19 @@ export function Diagnosis(props: DiagnosisProps) {
{diagnosisMessage(getDiagnosisCode(props.statusFlags))}
-
+
{t("Click here to learn more about connectivity codes.")}
-
+
{t("Click here for document to show to your IT department.")}
diff --git a/frontend/devices/connectivity/qos_panel.tsx b/frontend/devices/connectivity/qos_panel.tsx
index a2012a2f51..16d7ddfc58 100644
--- a/frontend/devices/connectivity/qos_panel.tsx
+++ b/frontend/devices/connectivity/qos_panel.tsx
@@ -88,7 +88,11 @@ export class QosPanel extends React.Component {
+ docLinkClick({
+ slug: "connecting-farmbot-to-the-internet",
+ navigate: this.navigate,
+ dispatch: this.props.dispatch,
+ })}>
{t("Learn more about connecting")}
diff --git a/frontend/farm_designer/map/easter_eggs/__tests__/bugs_test.tsx b/frontend/farm_designer/map/easter_eggs/__tests__/bugs_test.tsx
index 87f0c9dc8c..2a5e018783 100644
--- a/frontend/farm_designer/map/easter_eggs/__tests__/bugs_test.tsx
+++ b/frontend/farm_designer/map/easter_eggs/__tests__/bugs_test.tsx
@@ -6,7 +6,7 @@ import React from "react";
import { shallow, mount } from "enzyme";
import {
Bugs, BugsProps, showBugResetButton, showBugs, resetBugs, BugsControls,
- ExtraSettings,
+ BugsSettings,
} from "../bugs";
import { EggKeys, setEggStatus, getEggStatus } from "../status";
import { range } from "lodash";
@@ -112,10 +112,10 @@ describe("", () => {
});
});
-describe("", () => {
+describe("", () => {
it("toggles setting on", () => {
localStorage.setItem(EggKeys.BRING_ON_THE_BUGS, "");
- const wrapper = mount({ExtraSettings("surprise")} );
+ const wrapper = mount();
expect(wrapper.text().toLowerCase()).toContain("bug");
wrapper.find("button").last().simulate("click");
expect(localStorage.getItem(EggKeys.BRING_ON_THE_BUGS)).toEqual("true");
@@ -123,7 +123,7 @@ describe("", () => {
it("toggles setting off", () => {
localStorage.setItem(EggKeys.BRING_ON_THE_BUGS, "true");
- const wrapper = mount({ExtraSettings("surprise")} );
+ const wrapper = mount();
expect(wrapper.text().toLowerCase()).toContain("bug");
wrapper.find("button").last().simulate("click");
expect(localStorage.getItem(EggKeys.BRING_ON_THE_BUGS)).toEqual("");
diff --git a/frontend/farm_designer/map/easter_eggs/bugs.tsx b/frontend/farm_designer/map/easter_eggs/bugs.tsx
index 98f7359796..cd0698c762 100644
--- a/frontend/farm_designer/map/easter_eggs/bugs.tsx
+++ b/frontend/farm_designer/map/easter_eggs/bugs.tsx
@@ -6,7 +6,6 @@ import { getEggStatus, setEggStatus, EggKeys } from "./status";
import { t } from "../../../i18next_wrapper";
import { Row, ToggleButton } from "../../../ui";
import { BUGS, FilePath, Bug as BugSlug } from "../../../internal_urls";
-import { showByEveryTerm } from "../../../settings";
export interface BugsProps {
mapTransformProps: MapTransformProps;
@@ -133,19 +132,28 @@ export const BugsControls = () =>
: ;
-const Setting = (title: string, key: string, value: string) => {
- const on = localStorage.getItem(key) == value;
+interface SettingProps {
+ title: string;
+ storageKey: string;
+ value: string;
+}
+
+const Setting = (props: SettingProps) => {
+ const { title, storageKey, value } = props;
+ const on = localStorage.getItem(storageKey) == value;
return
localStorage.setItem(key, on ? "" : value)} />
+ toggleAction={() => localStorage.setItem(storageKey, on ? "" : value)} />
;
};
-export const ExtraSettings = (searchTerm: string) => {
- return showByEveryTerm("surprise", searchTerm) &&
-
- {Setting("Bug Attack", EggKeys.BRING_ON_THE_BUGS, "true")}
- ;
+export const BugsSettings = () => {
+ return
+
+ ;
};
diff --git a/frontend/farmware/farmware_forms.tsx b/frontend/farmware/farmware_forms.tsx
index 7634257e90..459174bb7b 100644
--- a/frontend/farmware/farmware_forms.tsx
+++ b/frontend/farmware/farmware_forms.tsx
@@ -188,7 +188,8 @@ export class FarmwareForm
const collapsedConfigs = farmware.config.filter(collapsed);
return
- {this.props.docPage && }
+ {this.props.docPage &&
+ }
|