diff --git a/app/controllers/api/rmq_utils_controller.rb b/app/controllers/api/rmq_utils_controller.rb
index 7fa1b4aef..a25c4c191 100644
--- a/app/controllers/api/rmq_utils_controller.rb
+++ b/app/controllers/api/rmq_utils_controller.rb
@@ -57,6 +57,7 @@ module Api
status
status_v8
sync
+ telemetry
\\#
\\*
).map { |x| x + "(\\.|\\z)" }.join("|")
diff --git a/app/lib/amqp_log_parser.rb b/app/lib/amqp_log_parser.rb
index f5f6fed0d..d92c974dc 100644
--- a/app/lib/amqp_log_parser.rb
+++ b/app/lib/amqp_log_parser.rb
@@ -7,7 +7,7 @@ class AmqpLogParser < Mutations::Command
TOO_OLD = "fbos version is out of date"
DISCARD = "message type field is not the kind that gets saved in the DB"
NOT_HASH = "logs must be a hash"
-
+ NOT_JSON = "Invalid JSON. Use a JSON validator."
# I keep a Ruby copy of the JSON here for reference.
# This is what a log will look like after JSON.parse()
EXAMPLE_JSON = {
@@ -75,6 +75,8 @@ class AmqpLogParser < Mutations::Command
def set_payload!
# Parse from string to a Ruby hash (JSON)
@output.payload = JSON.parse(payload)
+ rescue JSON::ParserError
+ add_error :json, :not_json, NOT_JSON
end
def log
diff --git a/app/lib/log_service.rb b/app/lib/log_service.rb
index 6a577f4b2..6ada163dc 100644
--- a/app/lib/log_service.rb
+++ b/app/lib/log_service.rb
@@ -23,11 +23,16 @@ class LogService < AbstractServiceRunner
end
def maybe_deliver(data)
+ return unless data.valid?
+
violation = THROTTLE_POLICY.is_throttled(data.device_id)
- ok = data.valid? && !violation
+
+ if violation
+ return warn_user(data, violation)
+ end
data.device.auto_sync_transaction do
- ok ? deliver(data) : warn_user(data, violation)
+ deliver(data)
end
end
@@ -41,6 +46,6 @@ class LogService < AbstractServiceRunner
end
def warn_user(data, violation)
- data.device.maybe_throttle(violation)
+ violation && data.device.maybe_throttle(violation)
end
end
diff --git a/app/lib/telemetry_service.rb b/app/lib/telemetry_service.rb
new file mode 100644
index 000000000..e49696d56
--- /dev/null
+++ b/app/lib/telemetry_service.rb
@@ -0,0 +1,32 @@
+# A singleton that runs on a separate process than the web server.
+# Listens to *ALL* incoming logs and stores them to the DB.
+# Also handles throttling.
+class TelemetryService < AbstractServiceRunner
+ MESSAGE = "TELEMETRY MESSAGE FROM %s"
+ FAILURE = "FAILED TELEMETRY MESSAGE FROM %s"
+ THROTTLE_POLICY = ThrottlePolicy.new({
+ ThrottlePolicy::TimePeriod.new(1.minute) => 25,
+ ThrottlePolicy::TimePeriod.new(1.hour) => 250,
+ ThrottlePolicy::TimePeriod.new(1.day) => 1500,
+ })
+
+ def process(delivery_info, payload)
+ device_key = delivery_info
+ .routing_key
+ .split(".")[1]
+ json = JSON.parse(payload)
+ other_stuff = { device: device_key,
+ is_telemetry: true,
+ message: MESSAGE % device_key }
+ THROTTLE_POLICY.track(device_key)
+ violation = THROTTLE_POLICY.is_throttled(device_key)
+ unless violation
+ puts json.merge(other_stuff).to_json
+ end
+ rescue JSON::ParserError
+ puts ({ device: device_key,
+ is_telemetry: true,
+ bad_json: payload,
+ message: FAILURE % device_key }).to_json
+ end
+end
diff --git a/app/lib/throttle_policy.rb b/app/lib/throttle_policy.rb
index 78d55ac6a..9473de435 100644
--- a/app/lib/throttle_policy.rb
+++ b/app/lib/throttle_policy.rb
@@ -19,9 +19,9 @@ class ThrottlePolicy
def is_throttled(unique_id)
rules
.map do |rule|
- is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit
- is_violation ? Violation.new(rule) : nil
- end
+ is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit
+ is_violation ? Violation.new(rule) : nil
+ end
.compact
.max
end
diff --git a/app/models/device.rb b/app/models/device.rb
index 7ba613774..551c85ab2 100644
--- a/app/models/device.rb
+++ b/app/models/device.rb
@@ -114,10 +114,11 @@ class Device < ApplicationRecord
# Sets the `throttled_until` and `throttled_at` fields if unpopulated or
# the throttle time period increases. Notifies user of cooldown period.
def maybe_throttle(violation)
+ return unless violation
end_t = violation.ends_at
# Some log validation errors will result in until_time being `nil`.
- if (violation && (throttled_until.nil? || end_t > throttled_until))
- reload.update!(throttled_until: end_t,
+ if (throttled_until.nil? || end_t > throttled_until)
+ reload.update_attributes!(throttled_until: end_t,
throttled_at: Time.now)
refresh_cache
cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p")
diff --git a/app/models/transport.rb b/app/models/transport.rb
index 1498605e9..fd0349b0e 100644
--- a/app/models/transport.rb
+++ b/app/models/transport.rb
@@ -42,6 +42,14 @@ class Transport
.bind("amq.topic", routing_key: "bot.*.logs")
end
+ def telemetry_channel
+ @telemetry_channel ||= self
+ .connection
+ .create_channel
+ .queue("api_telemetry_workers")
+ .bind("amq.topic", routing_key: "bot.*.telemetry")
+ end
+
def resource_channel
@resource_channel ||= self
.connection
diff --git a/frontend/apology.tsx b/frontend/apology.tsx
index 9f17e3f71..dcfae73e7 100644
--- a/frontend/apology.tsx
+++ b/frontend/apology.tsx
@@ -17,7 +17,7 @@ export function Apology(_: {}) {
Page Error
We can't render this part of the page due to an unrecoverable error.
- Here are some thing you can try:
+ Here are some things you can try:
-
diff --git a/frontend/css/buttons.scss b/frontend/css/buttons.scss
index c219a8126..3eea82c6f 100644
--- a/frontend/css/buttons.scss
+++ b/frontend/css/buttons.scss
@@ -194,6 +194,15 @@
background-color: darken($panel_gray, 5%) !important;
}
}
+ &.panel-light-gray {
+ background-color: $panel_medium_light_gray;
+ box-shadow: 0 2px 0px 0px darken($panel_medium_light_gray, 12%);
+ &:focus,
+ &:hover,
+ &.active {
+ background-color: darken($panel_medium_light_gray, 5%) !important;
+ }
+ }
&.panel-blue {
background-color: $panel_blue;
box-shadow: 0 2px 0px 0px darken($panel_blue, 12%);
@@ -230,6 +239,24 @@
background-color: darken($panel_red, 5%) !important;
}
}
+ &.panel-magenta {
+ background-color: $magenta;
+ box-shadow: 0 2px 0px 0px darken($magenta, 12%);
+ &:focus,
+ &:hover,
+ &.active {
+ background-color: darken($magenta, 5%) !important;
+ }
+ }
+ &.panel-cyan {
+ background-color: $cyan;
+ box-shadow: 0 2px 0px 0px darken($cyan, 12%);
+ &:focus,
+ &:hover,
+ &.active {
+ background-color: darken($cyan, 5%) !important;
+ }
+ }
&.zoom {
padding: 8px 12px;
&.zoom-out {
diff --git a/frontend/css/colors.scss b/frontend/css/colors.scss
index 11edbb672..e061f6708 100644
--- a/frontend/css/colors.scss
+++ b/frontend/css/colors.scss
@@ -43,6 +43,7 @@ $panel_light_green: #f3f9f1;
$panel_yellow: #e99d18;
$panel_light_yellow: #fffaf0;
$panel_gray: #92a7b3;
+$panel_medium_light_gray: #e6e6e6;
$panel_light_gray: #f9fbfc;
$panel_blue: #026365;
$panel_light_blue: #f0f8f8;
diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss
index b04f8e439..6237c4210 100644
--- a/frontend/css/farm_designer/farm_designer.scss
+++ b/frontend/css/farm_designer/farm_designer.scss
@@ -181,14 +181,11 @@
transition: all 0.2s ease;
}
}
- %search-item {
+
+ .plant-search-item,
+ .group-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
- &:hover,
- &.hovered {
- background: darken($light_green, 10%);
- transition: background 0.2s ease;
- }
img {
margin: 0 1rem 0 0;
height: 4rem;
@@ -196,18 +193,6 @@
}
}
- .plant-search-item {
- @extend %search-item;
- }
-
- .group-search-item {
- @extend %search-item;
- &:hover,
- &.hovered {
- background: #d7eaea;
- }
- }
-
%panel-item-base {
text-align: right;
font-size: 1rem;
@@ -244,11 +229,6 @@
.point-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
- &:hover,
- &.hovered {
- background: darken($light_brown, 10%);
- transition: background 0.2s ease;
- }
.saucer {
display: inline-block;
margin: 0 1rem 0 0;
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index ed55427cc..adea32910 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -44,32 +44,93 @@
.panel-container {
overflow: hidden;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ transition: background 0.2s ease;
+ }
+ }
&.green-panel {
background-color: $panel_light_green;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_green, 10%);
+ }
+ }
}
&.cyan-panel {
background-color: $light_cyan;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($light_cyan, 10%);
+ }
+ }
}
&.brown-panel {
background-color: $panel_light_brown;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_brown, 10%);
+ }
+ }
}
&.magenta-panel {
background-color: $light_magenta;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($light_magenta, 10%);
+ }
+ }
}
+ &.light-gray-panel,
&.gray-panel {
background-color: $panel_light_gray;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_gray, 10%);
+ }
+ }
}
&.yellow-panel {
background-color: $panel_light_yellow;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_yellow, 10%);
+ }
+ }
}
&.blue-panel {
background-color: $panel_light_blue;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_blue, 10%);
+ }
+ }
}
&.teal-panel {
background-color: $panel_light_teal;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_teal, 10%);
+ }
+ }
}
&.red-panel {
background-color: $panel_light_red;
+ div[class*="search-item"] {
+ &:hover,
+ &.hovered {
+ background: darken($panel_light_red, 10%);
+ }
+ }
}
}
@@ -91,6 +152,9 @@
&.gray-panel {
background-color: $panel_gray;
}
+ &.light-gray-panel{
+ background-color: $panel_medium_light_gray;
+ }
&.yellow-panel {
background-color: $panel_yellow;
}
@@ -144,13 +208,21 @@
.panel-title {
height: 50px;
.back-arrow {
+ &.black-text{
+ color: $medium_gray;
+ }
float: left;
text-align: center;
font-size: 1.8rem;
width: 50px;
line-height: 50px;
&:hover {
- color: $white;
+ &.black-text{
+ color: $darker_gray !important;
+ }
+ &.white-text{
+ color: $white;
+ }
}
}
.title {
@@ -220,12 +292,18 @@
position: absolute;
z-index: 9;
width: 100%;
- background: $gray;
+ background: $panel_medium_light_gray;
padding: 0.5rem;
button {
margin: 0.5rem;
+ float: left;
}
- .buttonrow{
+ label {
+ min-width: -webkit-fill-available;
+ margin-bottom: 0px;
+ margin-left: .5rem;
+ }
+ .buttonrow {
float:left;
}
}
@@ -234,7 +312,7 @@
padding-right: 0;
padding-left: 0;
padding-bottom: 5rem;
- max-height: calc(100vh - 10rem);
+ max-height: calc(100vh - 13rem);
overflow-y: auto;
overflow-x: hidden;
.plant-search-item,
diff --git a/frontend/farm_designer/farm_events/add_farm_event.tsx b/frontend/farm_designer/farm_events/add_farm_event.tsx
index 9656feebb..36a5ff530 100644
--- a/frontend/farm_designer/farm_events/add_farm_event.tsx
+++ b/frontend/farm_designer/farm_events/add_farm_event.tsx
@@ -17,6 +17,7 @@ import {
} from "../plants/designer_panel";
import { variableList } from "../../sequences/locals_list/variable_support";
import { t } from "../../i18next_wrapper";
+import { Panel } from "../panel_header";
interface State {
uuid: string;
@@ -85,10 +86,10 @@ export class RawAddFarmEvent
}
placeholderTemplate(children: React.ReactChild | React.ReactChild[]) {
- return
+ return