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:
  1. 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