Browse Source

[mod] oscar: /preferences , engines tab: report engine times

* display the median time instead of the average.
* add a "Reliability" column (sum up the metrics and the checker results).
* the "selected language", "SafeSearch", "Time range" values are displayed as "broken" when the checker tests fail.
Alexandre Flament 4 years ago
parent
commit
7cfd8d900a
34 changed files with 841 additions and 52 deletions
  1. 0 1
      searx/__init__.py
  2. 2 1
      searx/search/checker/impl.py
  3. 66 0
      searx/static/themes/oscar/css/logicodev-dark.css
  4. 0 0
      searx/static/themes/oscar/css/logicodev-dark.min.css
  5. 0 0
      searx/static/themes/oscar/css/logicodev-dark.min.css.map
  6. 66 0
      searx/static/themes/oscar/css/logicodev.css
  7. 0 0
      searx/static/themes/oscar/css/logicodev.min.css
  8. 0 0
      searx/static/themes/oscar/css/logicodev.min.css.map
  9. 65 0
      searx/static/themes/oscar/css/pointhi.css
  10. 0 0
      searx/static/themes/oscar/css/pointhi.min.css
  11. 0 0
      searx/static/themes/oscar/css/pointhi.min.css.map
  12. 1 1
      searx/static/themes/oscar/js/searx.min.js
  13. 3 0
      searx/static/themes/oscar/src/less/logicodev-dark/oscar.less
  14. 62 2
      searx/static/themes/oscar/src/less/logicodev/preferences.less
  15. 2 0
      searx/static/themes/oscar/src/less/logicodev/variables.less
  16. 2 0
      searx/static/themes/oscar/src/less/pointhi/oscar.less
  17. 61 1
      searx/static/themes/oscar/src/less/pointhi/preferences.less
  18. 1 0
      searx/static/themes/oscar/src/less/pointhi/variables.less
  19. 72 1
      searx/static/themes/simple/css/searx-rtl.css
  20. 0 0
      searx/static/themes/simple/css/searx-rtl.min.css
  21. 72 1
      searx/static/themes/simple/css/searx.css
  22. 0 0
      searx/static/themes/simple/css/searx.min.css
  23. 1 1
      searx/static/themes/simple/js/searx.head.min.js
  24. 1 1
      searx/static/themes/simple/js/searx.min.js
  25. 3 0
      searx/static/themes/simple/less/definitions.less
  26. 2 1
      searx/static/themes/simple/less/preferences.less
  27. 2 0
      searx/static/themes/simple/less/style.less
  28. 67 1
      searx/static/themes/simple/less/toolkit.less
  29. 5 7
      searx/templates/oscar/macros.html
  30. 102 23
      searx/templates/oscar/preferences.html
  31. 12 0
      searx/templates/oscar/stats.html
  32. 6 2
      searx/templates/simple/macros.html
  33. 59 5
      searx/templates/simple/preferences.html
  34. 106 3
      searx/webapp.py

+ 0 - 1
searx/__init__.py

@@ -20,7 +20,6 @@ import searx.settings_loader
 from os import environ
 from os.path import realpath, dirname, join, abspath, isfile
 
-
 searx_dir = abspath(dirname(__file__))
 engine_dir = dirname(realpath(__file__))
 static_path = abspath(join(dirname(__file__), 'static'))

+ 2 - 1
searx/search/checker/impl.py

@@ -5,6 +5,7 @@ import types
 import functools
 import itertools
 from time import time
+from timeit import default_timer
 from urllib.parse import urlparse
 
 import re
@@ -386,7 +387,7 @@ class Checker:
         params = self.processor.get_params(search_query, engineref_category)
         if params is not None:
             counter_inc('engine', search_query.engineref_list[0].name, 'search', 'count', 'sent')
-            self.processor.search(search_query.query, params, result_container, time(), 5)
+            self.processor.search(search_query.query, params, result_container, default_timer(), 5)
         return result_container
 
     def get_result_container_tests(self, test_name: str, search_query: SearchQuery) -> ResultContainerTests:

+ 66 - 0
searx/static/themes/oscar/css/logicodev-dark.css

@@ -923,12 +923,78 @@ input.cursor-text {
   padding: 0.5rem 1rem;
   margin: 0rem 0 0 2rem;
   border: 1px solid #ddd;
+  box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
   background: white;
   font-size: 14px;
   font-weight: normal;
   z-index: 1000000;
 }
+td:hover .engine-tooltip,
 th:hover .engine-tooltip,
 .engine-tooltip:hover {
   display: inline-block;
 }
+/* stacked-bar-chart */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 3rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  flex-grow: 1;
+  align-items: center;
+  display: inline-flex;
+}
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;
+  text-align: right;
+}
+.stacked-bar-chart-base {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+.stacked-bar-chart-median {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: #000000;
+  border: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate80 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border: 1px solid rgba(0, 0, 0, 0.3);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate95 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-bottom: 1px dotted rgba(0, 0, 0, 0.5);
+  padding: 0;
+}
+.stacked-bar-chart-rate100 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-left: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.4rem 0;
+  width: 1px;
+}

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/logicodev-dark.min.css


File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/logicodev-dark.min.css.map


+ 66 - 0
searx/static/themes/oscar/css/logicodev.css

@@ -896,15 +896,81 @@ input.cursor-text {
   padding: 0.5rem 1rem;
   margin: 0rem 0 0 2rem;
   border: 1px solid #ddd;
+  box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
   background: white;
   font-size: 14px;
   font-weight: normal;
   z-index: 1000000;
 }
+td:hover .engine-tooltip,
 th:hover .engine-tooltip,
 .engine-tooltip:hover {
   display: inline-block;
 }
+/* stacked-bar-chart */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 3rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  flex-grow: 1;
+  align-items: center;
+  display: inline-flex;
+}
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;
+  text-align: right;
+}
+.stacked-bar-chart-base {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+.stacked-bar-chart-median {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: #d5d8d7;
+  border: 1px solid rgba(213, 216, 215, 0.9);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate80 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border: 1px solid rgba(213, 216, 215, 0.3);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate95 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-bottom: 1px dotted rgba(213, 216, 215, 0.5);
+  padding: 0;
+}
+.stacked-bar-chart-rate100 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-left: 1px solid rgba(213, 216, 215, 0.9);
+  padding: 0.4rem 0;
+  width: 1px;
+}
 /*Global*/
 body {
   background: #1d1f21 none !important;

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/logicodev.min.css


File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/logicodev.min.css.map


+ 65 - 0
searx/static/themes/oscar/css/pointhi.css

@@ -688,6 +688,71 @@ input[type=checkbox]:not(:checked) + .label_hide_if_checked + .label_hide_if_not
   z-index: 1000000;
 }
 th:hover .engine-tooltip,
+td:hover .engine-tooltip,
 .engine-tooltip:hover {
   display: inline-block;
 }
+/* stacked-bar-chart */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 3rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  flex-grow: 1;
+  align-items: center;
+  display: inline-flex;
+}
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;
+  text-align: right;
+}
+.stacked-bar-chart-base {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+.stacked-bar-chart-median {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: #000000;
+  border: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate80 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border: 1px solid rgba(0, 0, 0, 0.3);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate95 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-bottom: 1px dotted rgba(0, 0, 0, 0.5);
+  padding: 0;
+}
+.stacked-bar-chart-rate100 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-left: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.4rem 0;
+  width: 1px;
+}

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/pointhi.min.css


File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/pointhi.min.css.map


File diff suppressed because it is too large
+ 1 - 1
searx/static/themes/oscar/js/searx.min.js


+ 3 - 0
searx/static/themes/oscar/src/less/logicodev-dark/oscar.less

@@ -1,4 +1,7 @@
 @import "../logicodev/variables.less";
+
+@stacked-bar-chart: rgb(213, 216, 215, 1);
+
 @import "../logicodev/footer.less";
 @import "../logicodev/checkbox.less";
 @import "../logicodev/onoff.less";

+ 62 - 2
searx/static/themes/oscar/src/less/logicodev/preferences.less

@@ -20,12 +20,72 @@ input.cursor-text {
     padding: 0.5rem 1rem;
     margin: 0rem 0 0 2rem;
     border: 1px solid #ddd;
+    box-shadow: 2px 2px 2px 0px rgba(0,0,0,0.1);
     background: white;
     font-size: 14px;
     font-weight: normal;
     z-index: 1000000; 
 }
 
-th:hover .engine-tooltip, .engine-tooltip:hover {
+td:hover .engine-tooltip, th:hover .engine-tooltip, .engine-tooltip:hover {
     display: inline-block;
-}
+}
+
+/* stacked-bar-chart */
+.stacked-bar-chart {
+    margin: 0;
+    padding: 0 0.125rem 0 3rem;
+    width: 100%;
+    width: -moz-available;
+    width: -webkit-fill-available;
+    width: fill;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    flex-grow: 1;
+    align-items: center;
+    display: inline-flex;
+}
+
+.stacked-bar-chart-value {
+    width: 3rem;
+    display: inline-block;
+    position: absolute;
+    padding: 0 0.5rem;   
+    text-align: right;
+}
+
+.stacked-bar-chart-base {
+    display:flex;
+    flex-shrink: 0;
+    flex-grow: 0;
+    flex-basis: unset;
+}
+
+.stacked-bar-chart-median {
+    .stacked-bar-chart-base();
+    background: @stacked-bar-chart;
+    border: 1px solid fade(@stacked-bar-chart, 90%);
+    padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate80 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border: 1px solid fade(@stacked-bar-chart, 30%);
+    padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate95 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border-bottom: 1px dotted fade(@stacked-bar-chart, 50%);
+    padding: 0;
+}
+
+.stacked-bar-chart-rate100 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border-left: 1px solid fade(@stacked-bar-chart, 90%);
+    padding: 0.4rem 0;
+    width: 1px;
+}

+ 2 - 0
searx/static/themes/oscar/src/less/logicodev/variables.less

@@ -14,3 +14,5 @@
 @light-green: #01D7D4;
 @orange: #FFA92F;
 @dark-red: #c9432f;
+
+@stacked-bar-chart: rgb(0, 0, 0);

+ 2 - 0
searx/static/themes/oscar/src/less/pointhi/oscar.less

@@ -1,3 +1,5 @@
+@import "variables.less";
+
 @import "footer.less";
 
 @import "checkbox.less";

+ 61 - 1
searx/static/themes/oscar/src/less/pointhi/preferences.less

@@ -14,6 +14,66 @@
     z-index: 1000000; 
 }
 
-th:hover .engine-tooltip, .engine-tooltip:hover {
+th:hover .engine-tooltip, td:hover .engine-tooltip, .engine-tooltip:hover {
     display: inline-block;
 }
+
+/* stacked-bar-chart */
+.stacked-bar-chart {
+    margin: 0;
+    padding: 0 0.125rem 0 3rem;
+    width: 100%;
+    width: -moz-available;
+    width: -webkit-fill-available;
+    width: fill;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    flex-grow: 1;
+    align-items: center;
+    display: inline-flex;
+}
+
+.stacked-bar-chart-value {
+    width: 3rem;
+    display: inline-block;
+    position: absolute;
+    padding: 0 0.5rem;   
+    text-align: right;
+}
+
+.stacked-bar-chart-base {
+    display:flex;
+    flex-shrink: 0;
+    flex-grow: 0;
+    flex-basis: unset;
+}
+
+.stacked-bar-chart-median {
+    .stacked-bar-chart-base();
+    background: @stacked-bar-chart;
+    border: 1px solid fade(@stacked-bar-chart, 90%);
+    padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate80 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border: 1px solid fade(@stacked-bar-chart, 30%);
+    padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate95 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border-bottom: 1px dotted fade(@stacked-bar-chart, 50%);
+    padding: 0;
+}
+
+.stacked-bar-chart-rate100 {
+    .stacked-bar-chart-base();
+    background: transparent;
+    border-left: 1px solid fade(@stacked-bar-chart, 90%);
+    padding: 0.4rem 0;
+    width: 1px;
+}
+

+ 1 - 0
searx/static/themes/oscar/src/less/pointhi/variables.less

@@ -0,0 +1 @@
+@stacked-bar-chart: rgb(0, 0, 0);

+ 72 - 1
searx/static/themes/simple/css/searx-rtl.css

@@ -1,4 +1,4 @@
-/*! searx | 23-03-2021 |  */
+/*! searx | 21-04-2021 |  */
 /*
 * searx, A privacy-respecting, hackable metasearch engine
 *
@@ -692,6 +692,12 @@ html.js .show_if_nojs {
 .danger {
   background-color: #fae1e1;
 }
+.warning {
+  background: #faf5e1;
+}
+.success {
+  background: #e3fae1;
+}
 .badge {
   display: inline-block;
   color: #fff;
@@ -1147,6 +1153,69 @@ select:focus {
     transform: rotate(360deg);
   }
 }
+/* -- stacked bar chart -- */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 4rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  display: inline-flex;
+}
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;
+  text-align: right;
+}
+.stacked-bar-chart-base {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+.stacked-bar-chart-median {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: #000000;
+  border: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate80 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border: 1px solid rgba(0, 0, 0, 0.3);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate95 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-bottom: 1px dotted rgba(0, 0, 0, 0.5);
+  padding: 0;
+}
+.stacked-bar-chart-rate100 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-left: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.4rem 0;
+  width: 1px;
+}
 /*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
 .autocomplete {
   position: absolute;
@@ -1435,8 +1504,10 @@ select:focus {
   font-size: 14px;
   font-weight: normal;
   z-index: 1000000;
+  text-align: left;
 }
 #main_preferences th:hover .engine-tooltip,
+#main_preferences td:hover .engine-tooltip,
 #main_preferences .engine-tooltip:hover {
   display: inline-block;
 }

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/simple/css/searx-rtl.min.css


+ 72 - 1
searx/static/themes/simple/css/searx.css

@@ -1,4 +1,4 @@
-/*! searx | 23-03-2021 |  */
+/*! searx | 21-04-2021 |  */
 /*
 * searx, A privacy-respecting, hackable metasearch engine
 *
@@ -692,6 +692,12 @@ html.js .show_if_nojs {
 .danger {
   background-color: #fae1e1;
 }
+.warning {
+  background: #faf5e1;
+}
+.success {
+  background: #e3fae1;
+}
 .badge {
   display: inline-block;
   color: #fff;
@@ -1147,6 +1153,69 @@ select:focus {
     transform: rotate(360deg);
   }
 }
+/* -- stacked bar chart -- */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 4rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  display: inline-flex;
+}
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;
+  text-align: right;
+}
+.stacked-bar-chart-base {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+.stacked-bar-chart-median {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: #000000;
+  border: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate80 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border: 1px solid rgba(0, 0, 0, 0.3);
+  padding: 0.3rem 0;
+}
+.stacked-bar-chart-rate95 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-bottom: 1px dotted rgba(0, 0, 0, 0.5);
+  padding: 0;
+}
+.stacked-bar-chart-rate100 {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+  background: transparent;
+  border-left: 1px solid rgba(0, 0, 0, 0.9);
+  padding: 0.4rem 0;
+  width: 1px;
+}
 /*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
 .autocomplete {
   position: absolute;
@@ -1435,8 +1504,10 @@ select:focus {
   font-size: 14px;
   font-weight: normal;
   z-index: 1000000;
+  text-align: left;
 }
 #main_preferences th:hover .engine-tooltip,
+#main_preferences td:hover .engine-tooltip,
 #main_preferences .engine-tooltip:hover {
   display: inline-block;
 }

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/simple/css/searx.min.css


+ 1 - 1
searx/static/themes/simple/js/searx.head.min.js

@@ -1,4 +1,4 @@
-/*! simple/searx.min.js | 23-03-2021 |  */
+/*! simple/searx.min.js | 21-04-2021 |  */
 
 (function(t,e){"use strict";var a=e.currentScript||function(){var t=e.getElementsByTagName("script");return t[t.length-1]}();t.searx={touch:"ontouchstart"in t||t.DocumentTouch&&document instanceof DocumentTouch||false,method:a.getAttribute("data-method"),autocompleter:a.getAttribute("data-autocompleter")==="true",search_on_category_select:a.getAttribute("data-search-on-category-select")==="true",infinite_scroll:a.getAttribute("data-infinite-scroll")==="true",static_path:a.getAttribute("data-static-path"),translations:JSON.parse(a.getAttribute("data-translations"))};e.getElementsByTagName("html")[0].className=t.searx.touch?"js touch":"js"})(window,document);
 //# sourceMappingURL=searx.head.min.js.map

File diff suppressed because it is too large
+ 1 - 1
searx/static/themes/simple/js/searx.min.js


+ 3 - 0
searx/static/themes/simple/less/definitions.less

@@ -19,6 +19,9 @@
 @color-warning: #dbba34;
 @color-warning-background: lighten(@color-warning, 40%);
 
+@color-success: #42db34;
+@color-success-background: lighten(@color-success, 40%);
+
 /// General
 
 @color-font: #444;

+ 2 - 1
searx/static/themes/simple/less/preferences.less

@@ -105,9 +105,10 @@
     font-size: 14px;
     font-weight: normal;
     z-index: 1000000; 
+    text-align: left;
   }
 
-  th:hover .engine-tooltip, .engine-tooltip:hover {
+  th:hover .engine-tooltip, td:hover .engine-tooltip, .engine-tooltip:hover {
     display: inline-block;
   }
   

+ 2 - 0
searx/static/themes/simple/less/style.less

@@ -4,6 +4,8 @@
 * To convert "style.less" to "style.css" run: $make styles
 */
 
+@stacked-bar-chart: rgb(0, 0, 0);
+
 @import "normalize.less";
 
 @import "definitions.less";

+ 67 - 1
searx/static/themes/simple/less/toolkit.less

@@ -36,6 +36,14 @@ html.js .show_if_nojs {
   background-color: @color-error-background;
 }
 
+.warning {
+  background: @color-warning-background;
+}
+
+.success {
+  background: @color-success-background;
+}
+
 .badge {
   display: inline-block;
   color: #fff;
@@ -465,4 +473,62 @@ select {
 	-webkit-transform: rotate(360deg);
 	transform: rotate(360deg);
     }
-}
+}
+
+/* -- stacked bar chart -- */
+.stacked-bar-chart {
+  margin: 0;
+  padding: 0 0.125rem 0 4rem;
+  width: 100%;
+  width: -moz-available;
+  width: -webkit-fill-available;
+  width: fill;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  display: inline-flex;
+}
+
+.stacked-bar-chart-value {
+  width: 3rem;
+  display: inline-block;
+  position: absolute;
+  padding: 0 0.5rem;   
+  text-align: right;
+}
+
+.stacked-bar-chart-base {
+  display:flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  flex-basis: unset;
+}
+
+.stacked-bar-chart-median {
+  .stacked-bar-chart-base();
+  background: @stacked-bar-chart;
+  border: 1px solid fade(@stacked-bar-chart, 90%);
+  padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate80 {
+  .stacked-bar-chart-base();
+  background: transparent;
+  border: 1px solid fade(@stacked-bar-chart, 30%);
+  padding: 0.3rem 0;
+}
+
+.stacked-bar-chart-rate95 {
+  .stacked-bar-chart-base();
+  background: transparent;
+  border-bottom: 1px dotted fade(@stacked-bar-chart, 50%);
+  padding: 0;
+}
+
+.stacked-bar-chart-rate100 {
+  .stacked-bar-chart-base();
+  background: transparent;
+  border-left: 1px solid fade(@stacked-bar-chart, 90%);
+  padding: 0.4rem 0;
+  width: 1px;
+}

+ 5 - 7
searx/templates/oscar/macros.html

@@ -134,13 +134,11 @@ custom-select{% if rtl %}-rtl{% endif %}
 {%- endmacro %}
 
 {% macro support_toggle(supports) -%}
-    {%- if supports -%}
-    <span class="label label-success">
-        {{- _("supported") -}}
-    </span>
+    {%- if supports == '?' -%}
+    <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true" title="{{- _('broken') -}}"></span>{{- "" -}}
+    {%- elif supports -%}
+    <span class="glyphicon glyphicon-ok" aria-hidden="true" title="{{- _('supported') -}}"></span>{{- "" -}}
     {%- else -%}
-    <span class="label label-danger">
-        {{- _("not supported") -}}
-    </span>
+    <span aria-hidden="true" title="{{- _('not supported') -}}"></span>{{- "" -}}
     {%- endif -%}
 {%- endmacro %}

+ 102 - 23
searx/templates/oscar/preferences.html

@@ -1,16 +1,92 @@
 {% from 'oscar/macros.html' import preferences_item_header, preferences_item_header_rtl, preferences_item_footer, preferences_item_footer_rtl, checkbox_toggle, support_toggle, custom_select_class %}
 {% extends "oscar/base.html" %}
-{% macro engine_about(search_engine, id) -%}
-{% if search_engine.about is defined %}
+{%- macro engine_about(search_engine, id) -%}
+{% if search_engine.about is defined or stats[search_engine.name]['result_count'] > 0 %}
 {% set about = search_engine.about %}
 <div class="engine-tooltip" role="tooltip" id="{{ id }}">{{- "" -}}
-    <h5><a href="{{about.website}}" rel="noreferrer">{{about.website}}</a></h5>
-    {%- if about.wikidata_id -%}<p><a href="https://www.wikidata.org/wiki/{{about.wikidata_id}}" rel="noreferrer">wikidata.org/wiki/{{about.wikidata_id}}</a></p>{%- endif -%}
+    {% if search_engine.about is defined %}
+        <h5><a href="{{about.website}}" rel="noreferrer">{{about.website}}</a></h5>
+        {%- if about.wikidata_id -%}<p><a href="https://www.wikidata.org/wiki/{{about.wikidata_id}}" rel="noreferrer">wikidata.org/wiki/{{about.wikidata_id}}</a></p>{%- endif -%}
+    {% endif %}
     {%- if search_engine.enable_http %}<p>{{ icon('exclamation-sign', 'No HTTPS') }}{{ _('No HTTPS')}}</p>{% endif -%}
+    {%- if stats[search_engine.name]['result_count'] -%}
+        <p>{{ _('Number of results') }}: {{ stats[search_engine.name]['result_count'] }} ( {{ _('Avg.') }} )</p>{{- "" -}}
+    {%- endif -%}
 </div>
 {%- endif -%}
 {%- endmacro %}
-{% block title %}{{ _('preferences') }} - {% endblock %}
+
+{%- macro engine_time(engine_name, css_align_class) -%}
+<td class="{{ css_align_class }} {{ 'danger' if stats[engine_name]['warn_time'] else '' }}">
+    {%- if stats[engine_name].time != None -%}
+        <span aria-labelledby="{{engine_name}}_time">
+        {%- if stats[engine_name]['warn_time'] -%}
+            {{icon('exclamation-sign')}}
+        {%- endif -%}
+        {{- stats[engine_name].time -}}
+        </span>{{- "" -}}
+        <div class="engine-tooltip text-left" role="tooltip" id="{{engine_name}}_time">{{- "" -}}
+            <p>{{ _('Median') }}: {{ stats[engine_name].time }}</p>{{- "" -}}
+            <p>{{ _('P80') }}: {{ stats[engine_name].rate80 }}</p>{{- "" -}}
+            <p>{{ _('P95') }}: {{ stats[engine_name].rate95 }}</p>{{- "" -}}
+        </div>
+    {%- endif -%}
+</td>
+{%- endmacro -%}
+
+{%- macro engine_time(engine_name, css_align_class) -%}
+<td class="{{ label }}" style="padding: 2px">{{- "" -}}
+    {%- if stats[engine_name].time != None -%}
+    <span class="stacked-bar-chart-value">{{- stats[engine_name].time -}}</span>{{- "" -}}
+    <span class="stacked-bar-chart" aria-labelledby="{{engine_name}}_chart" aria-hidden="true">{{- "" -}}
+        <span style="width: calc(max(2px, 100%*{{ (stats[engine_name].time / max_rate95)|round(3) }}))" class="stacked-bar-chart-median"></span>{{- "" -}}
+        <span style="width: calc(100%*{{ ((stats[engine_name].rate80 - stats[engine_name].time) / max_rate95)|round(3) }})" class="stacked-bar-chart-rate80"></span>{{- "" -}}
+        <span style="width: calc(100%*{{ ((stats[engine_name].rate95 - stats[engine_name].rate80) / max_rate95)|round(3) }})" class="stacked-bar-chart-rate95"></span>{{- "" -}}
+        <span class="stacked-bar-chart-rate100"></span>{{- "" -}}
+    </span>{{- "" -}}
+    <div class="engine-tooltip text-left" role="tooltip" id="{{engine_name}}_graph">{{- "" -}}
+        <p>{{ _('Median') }}: {{ stats[engine_name].time }}</p>{{- "" -}}
+        <p>{{ _('P80') }}: {{ stats[engine_name].rate80 }}</p>{{- "" -}}
+        <p>{{ _('P95') }}: {{ stats[engine_name].rate95 }}</p>{{- "" -}}
+    </div>
+    {%- endif -%}
+</td>
+{%- endmacro -%}
+
+{%- macro engine_reliability(engine_name, css_align_class) -%}
+{% set r = reliabilities.get(engine_name, {}).get('reliablity', None) %}
+{% set checker_result = reliabilities.get(engine_name, {}).get('checker', []) %}
+{% set errors = reliabilities.get(engine_name, {}).get('errors', []) %}
+{% if r != None %}
+    {% if r <= 50 %}{% set label = 'danger' %}
+    {% elif r < 80 %}{% set label = 'warning' %}
+    {% elif r < 90 %}{% set label = 'default' %}
+    {% else %}{% set label = 'success' %}
+    {% endif %}
+{% else %}
+    {% set r = '' %}
+{% endif %}
+{% if checker_result or errors %}
+<td class="{{ css_align_class }} {{ label }}">{{- "" -}}
+    <span aria-labelledby="{{engine_name}}_reliablity">
+        {%- if reliabilities[engine_name].checker %}{{ icon('exclamation-sign', 'The checker fails on the some tests') }}{% endif %} {{ r -}}
+    </span>{{- "" -}}
+    <div class="engine-tooltip text-left" role="tooltip" id="{{engine_name}}_reliablity">
+        {%- if checker_result -%}
+        <p>{{ _("Failed checker test(s): ") }} {{ ', '.join(checker_result) }}</p>
+        {%- endif -%}
+        {%- for error in errors -%}
+        <p>{{ error }} </p>{{- "" -}}
+        {%- endfor -%}
+    </div>{{- "" -}}
+</td>
+{%- else -%}
+<td class="{{ css_align_class }} {{ label }}"><span>{{ r }}</span></td>
+{%- endif -%}
+{%- endmacro -%}
+
+{%- block title %}{{ _('preferences') }} - {% endblock -%}
+
 {% block content %}
 
 <div>
@@ -182,7 +258,6 @@
                 </fieldset>
             </div>
             <div class="tab-pane active_if_nojs" id="tab_engine">
-
                 <!-- Nav tabs -->
                 <ul class="nav nav-tabs nav-justified hide_if_nojs" role="tablist">
                     {% for categ in all_categories %}
@@ -217,14 +292,16 @@
                                     <th scope="col">{{ _("Allow") }}</th>
                                     <th scope="col">{{ _("Engine name") }}</th>
                                     <th scope="col">{{ _("Shortcut") }}</th>
-                                    <th scope="col">{{ _("Selected language") }}</th>
-                                    <th scope="col">{{ _("SafeSearch") }}</th>
-                                    <th scope="col">{{ _("Time range") }}</th>
-                                    <th scope="col">{{ _("Avg. time") }}</th>
-                                    <th scope="col">{{ _("Max time") }}</th>
+                                    <th scope="col" style="width: 10rem">{{ _("Selected language") }}</th>
+                                    <th scope="col" style="width: 10rem">{{ _("SafeSearch") }}</th>
+                                    <th scope="col" style="width: 10rem">{{ _("Time range") }}</th>
+                                    <th scope="col">{{ _("Response time") }}</th>
+                                    <th scope="col" class="text-right"  style="width: 7rem">{{ _("Max time") }}</th>
+                                    <th scope="col" class="text-right" style="width: 7rem">{{ _("Reliablity") }}</th>
                                     {% else %}
-                                    <th scope="col" class="text-right">{{ _("Max time") }}</th>
-                                    <th scope="col" class="text-right">{{ _("Avg. time") }}</th>
+                                    <th scope="col">{{ _("Reliablity") }}</th>
+                                    <th scope="col">{{ _("Max time") }}</th>
+                                    <th scope="col" class="text-right">{{ _("Response time") }}</th>
                                     <th scope="col" class="text-right">{{ _("Time range") }}</th>
                                     <th scope="col" class="text-right">{{ _("SafeSearch") }}</th>
                                     <th scope="col" class="text-right">{{ _("Selected language") }}</th>
@@ -246,17 +323,19 @@
                                             {{- engine_about(search_engine, 'tooltip_' + categ + '_' + search_engine.name) -}}
                                         </th>
                                         <td class="name">{{ shortcuts[search_engine.name] }}</td>
-                                        <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
-                                        <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
-                                        <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
-                                        <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{% if stats[search_engine.name]['warn_time'] %}{{ icon('exclamation-sign')}} {% endif %}{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
-                                        <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{% if stats[search_engine.name]['warn_timeout'] %}{{ icon('exclamation-sign') }} {% endif %}{{ search_engine.timeout }}</td>
+                                        <td>{{ support_toggle(supports[search_engine.name]['supports_selected_language']) }}</td>
+                                        <td>{{ support_toggle(supports[search_engine.name]['safesearch']) }}</td>
+                                        <td>{{ support_toggle(supports[search_engine.name]['time_range_support']) }}</td>
+                                        {{ engine_time(search_engine.name, 'text-right') }}
+                                        <td class="text-right {{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{% if stats[search_engine.name]['warn_timeout'] %}{{ icon('exclamation-sign') }} {% endif %}{{ search_engine.timeout }}</td>
+                                        {{ engine_reliability(search_engine.name, 'text-right ') }}
                                     {% else %}
-                                        <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}{% if stats[search_engine.name]['warn_time'] %} {{ icon('exclamation-sign')}}{% endif %}</td>
-                                        <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}{% if stats[search_engine.name]['warn_time'] %} {{ icon('exclamation-sign')}}{% endif %}</td>
-                                        <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
-                                        <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
-                                        <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
+                                        {{ engine_reliability(search_engine.name, 'text-left') }}
+                                        <td class="text-left {{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}{% if stats[search_engine.name]['warn_time'] %} {{ icon('exclamation-sign')}}{% endif %}</td>
+                                        {{ engine_time(search_engine.name, 'text-left') }}
+                                        <td>{{ support_toggle(supports[search_engine.name]['time_range_support']) }}</td>
+                                        <td>{{ support_toggle(supports[search_engine.name]['safesearch']) }}</td>
+                                        <td>{{ support_toggle(supports[search_engine.name]['supports_selected_language']) }}</td>
                                         <td>{{ shortcuts[search_engine.name] }}</td>
                                         <th scope="row"><span>{% if search_engine.enable_http %}{{ icon('exclamation-sign', 'No HTTPS') }}{% endif %}{{ search_engine.name }}</span>{{ engine_about(search_engine) }}</th>
                                         <td class="onoff-checkbox">

+ 12 - 0
searx/templates/oscar/stats.html

@@ -1,4 +1,16 @@
 {% extends "oscar/base.html" %}
+{% block styles %}
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/charts.min.css') }}" type="text/css" />
+    <style>
+        #engine-times {
+          --labels-size: 20rem;
+        }
+
+        #engine-times th {
+            text-align: right;
+        }
+    </style>
+{% endblock %}
 {% block title %}{{ _('stats') }} - {% endblock %}
 {% block content %}
 <div class="container-fluid">

+ 6 - 2
searx/templates/simple/macros.html

@@ -79,7 +79,11 @@
 
 {%- macro checkbox(name, checked, readonly, disabled) -%}
 <div class="checkbox">{{- '' -}}
-    <input type="checkbox" value="None" id="{{ name }}" name="{{ name }}" {% if checked %}checked{% endif %}{% if readonly %} readonly="readonly" {% endif %}{% if disabled %} disabled="disabled" {% endif %}/>{{- '' -}}
-    <label for="{{ name }}"></label>{{- '' -}}
+    {%- if checked == '?' -%}
+      {{ icon_small('warning') }}
+    {%- else -%}
+      <input type="checkbox" value="None" id="{{ name }}" name="{{ name }}" {% if checked %}checked{% endif %}{% if readonly %} readonly="readonly" {% endif %}{% if disabled %} disabled="disabled" {% endif %}/>{{- '' -}}
+      <label for="{{ name }}"></label>{{- '' -}}
+    {%- endif -%}
 </div>
 {%- endmacro -%}

+ 59 - 5
searx/templates/simple/preferences.html

@@ -29,6 +29,58 @@
 {%- endif -%}
 {%- endmacro %}
 
+{%- macro engine_time(engine_name) -%}
+<td class="{{ label }}" style="padding: 2px; width: 13rem;">{{- "" -}}
+    {%- if stats[engine_name].time != None -%}
+    <span class="stacked-bar-chart-value">{{- stats[engine_name].time -}}</span>{{- "" -}}
+    <span class="stacked-bar-chart" aria-labelledby="{{engine_name}}_chart" aria-hidden="true">{{- "" -}}
+        <span style="width: calc(max(2px, 100%*{{ (stats[engine_name].time / max_rate95)|round(3) }}))" class="stacked-bar-chart-median"></span>{{- "" -}}
+        <span style="width: calc(100%*{{ ((stats[engine_name].rate80 - stats[engine_name].time) / max_rate95)|round(3) }})" class="stacked-bar-chart-rate80"></span>{{- "" -}}
+        <span style="width: calc(100%*{{ ((stats[engine_name].rate95 - stats[engine_name].rate80) / max_rate95)|round(3) }})" class="stacked-bar-chart-rate95"></span>{{- "" -}}
+        <span class="stacked-bar-chart-rate100"></span>{{- "" -}}
+    </span>{{- "" -}}
+    <div class="engine-tooltip text-left" role="tooltip" id="{{engine_name}}_graph">{{- "" -}}
+        <p>{{ _('Median') }}: {{ stats[engine_name].time }}</p>{{- "" -}}
+        <p>{{ _('P80') }}: {{ stats[engine_name].rate80 }}</p>{{- "" -}}
+        <p>{{ _('P95') }}: {{ stats[engine_name].rate95 }}</p>{{- "" -}}
+    </div>
+    {%- endif -%}
+</td>
+{%- endmacro -%}
+
+{%- macro engine_reliability(engine_name) -%}
+{% set r = reliabilities.get(engine_name, {}).get('reliablity', None) %}
+{% set checker_result = reliabilities.get(engine_name, {}).get('checker', []) %}
+{% set errors = reliabilities.get(engine_name, {}).get('errors', []) %}
+{% if r != None %}
+    {% if r <= 50 %}{% set label = 'danger' %}
+    {% elif r < 80 %}{% set label = 'warning' %}
+    {% elif r < 90 %}{% set label = '' %}
+    {% else %}{% set label = 'success' %}
+    {% endif %}
+{% else %}
+    {% set r = '' %}
+{% endif %}
+{% if checker_result or errors %}
+<td class="{{ label }}">{{- "" -}}
+    <span aria-labelledby="{{engine_name}}_reliablity">
+        {%- if reliabilities[engine_name].checker %}{{ icon('warning', 'The checker fails on the some tests') }}{% endif %} {{ r -}}
+    </span>{{- "" -}}
+    <div class="engine-tooltip" style="right: 12rem;" role="tooltip" id="{{engine_name}}_reliablity">
+        {%- if checker_result -%}
+        <p>{{ _("The checker fails on this tests: ") }} {{ ', '.join(checker_result) }}</p>
+        {%- endif -%}
+        {%- if errors %}<p>{{ _('Errors:') }}</p>{% endif -%}
+        {%- for error in errors -%}
+        <p>{{ error }} </p>{{- "" -}}
+        {%- endfor -%}
+    </div>{{- "" -}}
+</td>
+{%- else -%}
+<td class="{{ css_align_class }} {{ label }}"><span>{{ r }}</span></td>
+{%- endif -%}
+{%- endmacro -%}
+
 {% block head %} {% endblock %}
 {% block content %}
 
@@ -123,8 +175,9 @@
         <th>{{ _("Supports selected language") }}</th>
         <th>{{ _("SafeSearch") }}</th>
         <th>{{ _("Time range") }}</th>
-        <th>{{ _("Avg. time") }}</th>
+        <th>{{ _("Response time") }}</th>
         <th>{{ _("Max time") }}</th>
+        <th>{{ _("Reliablity") }}</th>
       </tr>
       {% for search_engine in engines_by_category[categ] %}
 
@@ -134,11 +187,12 @@
         <td class="engine_checkbox">{{ checkbox_onoff(engine_id, (search_engine.name, categ) in disabled_engines) }}</td>
         <th class="name">{% if search_engine.enable_http %}{{ icon('warning', 'No HTTPS') }}{% endif %} {{ search_engine.name }} {{ engine_about(search_engine) }}</th>
         <td class="shortcut">{{ shortcuts[search_engine.name] }}</td>
-        <td>{{ checkbox(engine_id + '_supported_languages', current_language == 'all' or current_language in search_engine.supported_languages or current_language.split('-')[0] in search_engine.supported_languages, true, true) }}</td>
-        <td>{{ checkbox(engine_id + '_safesearch', search_engine.safesearch==True, true, true) }}</td>
-        <td>{{ checkbox(engine_id + '_time_range_support', search_engine.time_range_support==True, true, true) }}</td>
-        <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
+        <td>{{ checkbox(engine_id + '_supported_languages', supports[search_engine.name]['supports_selected_language'], true, true) }}</td>
+        <td>{{ checkbox(engine_id + '_safesearch', supports[search_engine.name]['safesearch'], true, true) }}</td>
+        <td>{{ checkbox(engine_id + '_time_range_support', supports[search_engine.name]['time_range_support'], true, true) }}</td>
+        {{ engine_time(search_engine.name) }}
         <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}</td>
+        {{ engine_reliability(search_engine.name) }}
       </tr>
       {% endif %}
       {% endfor %}

+ 106 - 3
searx/webapp.py

@@ -93,7 +93,7 @@ from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES
 from searx.answerers import answerers
 from searx.network import stream as http_stream
 from searx.answerers import ask
-from searx.metrics import get_engines_stats, get_engine_errors, histogram
+from searx.metrics import get_engines_stats, get_engine_errors, histogram, counter
 
 # serve pages with HTTP/1.1
 from werkzeug.serving import WSGIRequestHandler
@@ -170,6 +170,31 @@ _category_names = (gettext('files'),
                    gettext('onions'),
                    gettext('science'))
 
+#
+exception_classname_to_label = {
+    "searx.exceptions.SearxEngineCaptchaException": gettext("CAPTCHA"),
+    "searx.exceptions.SearxEngineTooManyRequestsException": gettext("too many requests"),
+    "searx.exceptions.SearxEngineAccessDeniedException": gettext("access denied"),
+    "searx.exceptions.SearxEngineAPIException": gettext("server API error"),
+    "httpx.TimeoutException": gettext("HTTP timeout"),
+    "httpx.ConnectTimeout": gettext("HTTP timeout"),
+    "httpx.ReadTimeout": gettext("HTTP timeout"),
+    "httpx.WriteTimeout": gettext("HTTP timeout"),
+    "httpx.HTTPStatusError": gettext("HTTP error"),
+    "httpx.ConnectError": gettext("HTTP connection error"),
+    "httpx.RemoteProtocolError": gettext("HTTP protocol error"),
+    "httpx.LocalProtocolError": gettext("HTTP protocol error"),
+    "httpx.ProtocolError": gettext("HTTP protocol error"),
+    "httpx.ReadError": gettext("network error"),
+    "httpx.WriteError": gettext("network error"),
+    "httpx.ProxyError": gettext("proxy error"),
+    "searx.exceptions.SearxEngineXPathException": gettext("parsing error"),
+    "KeyError": gettext("parsing error"),
+    "json.decoder.JSONDecodeError": gettext("parsing error"),
+    "lxml.etree.ParserError": gettext("parsing error"),
+    None: gettext("unexpected crash"),
+}
+
 _flask_babel_get_translations = flask_babel.get_translations
 
 
@@ -855,26 +880,101 @@ def preferences():
     engines_by_category = {}
     for c in categories:
         engines_by_category[c] = [e for e in categories[c] if e.name in filtered_engines]
+        # sort the engines alphabetically since the order in settings.yml is meaningless.
+        list.sort(engines_by_category[c], key=lambda e: e.name)
 
     # get first element [0], the engine time,
     # and then the second element [1] : the time (the first one is the label)
     stats = {}
+    max_rate95 = 0
     for _, e in filtered_engines.items():
         h = histogram('engine', e.name, 'time', 'total')
         median = round(h.percentage(50), 1) if h.count > 0 else None
+        rate80 = round(h.percentage(80), 1) if h.count > 0 else None
+        rate95 = round(h.percentage(95), 1) if h.count > 0 else None
+
+        max_rate95 = max(max_rate95, rate95 or 0)
+
+        result_count_sum = histogram('engine', e.name, 'result', 'count').sum
+        successful_count = counter('engine', e.name, 'search', 'count', 'successful')
+        result_count = int(result_count_sum / float(successful_count)) if successful_count else 0
 
         stats[e.name] = {
             'time': median if median else None,
-            'warn_time': median is not None and median > settings['outgoing']['request_timeout'],
+            'rate80': rate80 if rate80 else None,
+            'rate95': rate95 if rate95 else None,
             'warn_timeout': e.timeout > settings['outgoing']['request_timeout'],
-            'supports_selected_language': _is_selected_language_supported(e, request.preferences)
+            'supports_selected_language': _is_selected_language_supported(e, request.preferences),
+            'result_count': result_count,
         }
     # end of stats
 
+    # reliabilities
+    reliabilities = {}
+    engine_errors = get_engine_errors(filtered_engines)
+    checker_results = checker_get_result()
+    checker_results = checker_results['engines'] \
+        if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
+    for _, e in filtered_engines.items():
+        checker_result = checker_results.get(e.name, {})
+        checker_success = checker_result.get('success', True)
+        errors = engine_errors.get(e.name) or []
+        if counter('engine', e.name, 'search', 'count', 'sent') == 0:
+            # no request
+            reliablity = None
+        elif checker_success and not errors:
+            reliablity = 100
+        elif 'simple' in checker_result.get('errors', {}):
+            # the basic (simple) test doesn't work: the engine is broken accoding to the checker
+            # even if there is no exception
+            reliablity = 0
+        else:
+            reliablity = 100 - sum([error['percentage'] for error in errors if not error.get('secondary')])
+
+        reliabilities[e.name] = {
+            'reliablity': reliablity,
+            'errors': [],
+            'checker': checker_results.get(e.name, {}).get('errors', {}).keys(),
+        }
+        # keep the order of the list checker_results[e.name]['errors'] and deduplicate.
+        # the first element has the highest percentage rate.
+        reliabilities_errors = []
+        for error in errors:
+            error_user_message = None
+            if error.get('secondary') or 'exception_classname' not in error:
+                continue
+            error_user_message = exception_classname_to_label.get(error.get('exception_classname'))
+            if not error:
+                error_user_message = exception_classname_to_label[None]
+            if error_user_message not in reliabilities_errors:
+                reliabilities_errors.append(error_user_message)
+        reliabilities[e.name]['errors'] = reliabilities_errors
+
+    # supports
+    supports = {}
+    for _, e in filtered_engines.items():
+        supports_selected_language = _is_selected_language_supported(e, request.preferences)
+        safesearch = e.safesearch
+        time_range_support = e.time_range_support
+        for checker_test_name in checker_results.get(e.name, {}).get('errors', {}):
+            if supports_selected_language and checker_test_name.startswith('lang_'):
+                supports_selected_language = '?'
+            elif safesearch and checker_test_name == 'safesearch':
+                safesearch = '?'
+            elif time_range_support and checker_test_name == 'time_range':
+                time_range_support = '?'
+        supports[e.name] = {
+            'supports_selected_language': supports_selected_language,
+            'safesearch': safesearch,
+            'time_range_support': time_range_support,
+        }
+
+    #
     locked_preferences = list()
     if 'preferences' in settings and 'lock' in settings['preferences']:
         locked_preferences = settings['preferences']['lock']
 
+    #
     return render('preferences.html',
                   selected_categories=get_selected_categories(request.preferences, request.form),
                   all_categories=_get_ordered_categories(),
@@ -883,6 +983,9 @@ def preferences():
                   image_proxy=image_proxy,
                   engines_by_category=engines_by_category,
                   stats=stats,
+                  max_rate95=max_rate95,
+                  reliabilities=reliabilities,
+                  supports=supports,
                   answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers],
                   disabled_engines=disabled_engines,
                   autocomplete_backends=autocomplete_backends,

Some files were not shown because too many files changed in this diff