robotframework-dashboard 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {robotframework_dashboard-2.1.0/robotframework_dashboard.egg-info → robotframework_dashboard-2.2.0}/PKG-INFO +1 -1
  2. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/README.md +2 -1
  3. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/arguments.py +92 -4
  4. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/css/base.css +16 -0
  5. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/css/components.css +156 -71
  6. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/css/dark.css +0 -1
  7. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/database.py +96 -21
  8. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/common.js +114 -1
  9. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/customsections.js +12 -77
  10. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/overview.js +10 -0
  11. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/statistics.js +9 -4
  12. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/layout.js +105 -4
  13. robotframework_dashboard-2.2.0/robotframework_dashboard/js/linkwidgets.js +196 -0
  14. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/localstorage.js +4 -2
  15. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/log.js +8 -1
  16. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/statwidgets.js +158 -63
  17. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/theme.js +36 -1
  18. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/graphmetadata.js +26 -0
  19. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/information.js +9 -0
  20. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/svg.js +16 -0
  21. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/main.py +1 -0
  22. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/processors.py +9 -1
  23. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/queries.py +2 -0
  24. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/robotdashboard.py +18 -1
  25. robotframework_dashboard-2.2.0/robotframework_dashboard/robotdashboardlistener.py +327 -0
  26. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/server.py +80 -8
  27. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/templates/admin.html +1 -1
  28. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/templates/dashboard.html +109 -12
  29. robotframework_dashboard-2.2.0/robotframework_dashboard/version.py +1 -0
  30. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0/robotframework_dashboard.egg-info}/PKG-INFO +1 -1
  31. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/SOURCES.txt +2 -0
  32. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/setup.py +1 -1
  33. robotframework_dashboard-2.1.0/robotframework_dashboard/version.py +0 -1
  34. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/LICENSE +0 -0
  35. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/MANIFEST.in +0 -0
  36. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/pyproject.toml +0 -0
  37. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/__init__.py +0 -0
  38. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/abstractdb.py +0 -0
  39. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/css/colors.css +0 -0
  40. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dashboard.py +0 -0
  41. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/bootstrap.css +0 -0
  42. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/bootstrap.js +0 -0
  43. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/chart.js +0 -0
  44. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/chartjs-adapter-date-fns.js +0 -0
  45. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/chartjs-chart-boxplot.js +0 -0
  46. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/chartjs-chart-matrix.js +0 -0
  47. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/chartjs-plugin-datalabels.js +0 -0
  48. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/datatables.css +0 -0
  49. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/datatables.js +0 -0
  50. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/gridstack.css +0 -0
  51. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/gridstack.js +0 -0
  52. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies/pako.js +0 -0
  53. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/dependencies.py +0 -0
  54. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_api.js +0 -0
  55. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_common.js +0 -0
  56. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_eventlisteners.js +0 -0
  57. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_information.js +0 -0
  58. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_main.js +0 -0
  59. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/admin_page/admin_theme.js +0 -0
  60. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/database.js +0 -0
  61. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/eventlisteners.js +0 -0
  62. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/filter.js +0 -0
  63. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/all.js +0 -0
  64. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/chart_factory.js +0 -0
  65. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/compare.js +0 -0
  66. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/config_helpers.js +0 -0
  67. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/keyword.js +0 -0
  68. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/run.js +0 -0
  69. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/suite.js +0 -0
  70. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/tables.js +0 -0
  71. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_creation/test.js +0 -0
  72. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/donut.js +0 -0
  73. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/duration.js +0 -0
  74. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/duration_deviation.js +0 -0
  75. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/failed.js +0 -0
  76. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/flaky.js +0 -0
  77. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/graph_config.js +0 -0
  78. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/heatmap.js +0 -0
  79. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/helpers.js +0 -0
  80. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/messages.js +0 -0
  81. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/stats.js +0 -0
  82. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/time_consuming.js +0 -0
  83. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/graph_data/tooltip_helpers.js +0 -0
  84. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/information.js +0 -0
  85. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/main.js +0 -0
  86. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/menu.js +0 -0
  87. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/chartconfig.js +0 -0
  88. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/data.js +0 -0
  89. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/globals.js +0 -0
  90. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/graphs.js +0 -0
  91. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/settings.js +0 -0
  92. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/js/variables/statwidgetdefs.js +0 -0
  93. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard/licenses/THIRD_PARTY_LICENSES.txt +0 -0
  94. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/dependency_links.txt +0 -0
  95. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/entry_points.txt +0 -0
  96. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/not-zip-safe +0 -0
  97. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/requires.txt +0 -0
  98. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/robotframework_dashboard.egg-info/top_level.txt +0 -0
  99. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-dashboard
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Output processor and dashboard generator for Robot Framework output files
5
5
  Home-page: https://github.com/marketsquare/robotframework-dashboard
6
6
  Author: Tim de Groot
@@ -78,6 +78,7 @@ For detailed usage instructions, advanced examples, and full documentation, visi
78
78
  - 🔍 [**Filtering**](https://marketsquare.github.io/robotframework-dashboard/filtering.html) - Apply filters to analyze trends in your test data and highlight specific tags, amounts or datetime ranges.
79
79
  - 🎨 [**Customization**](https://marketsquare.github.io/robotframework-dashboard/customization.html) - Customize dashboard sections, graph layouts, and visualizations to suit your workflow.
80
80
  - ⚙️ [**Settings**](https://marketsquare.github.io/robotframework-dashboard/settings.html) - Configure dashboard preferences including themes, default views, graph options, and save your settings for consistent team-wide use.
81
+ - 📐 [**Architecture**](https://marketsquare.github.io/robotframework-dashboard/architecture.html) - See how robotdashboard is structured internally, from CLI invocation to a rendered dashboard, in both Regular Mode and Server Mode.
81
82
  - 🖥️ [**Dashboard Server**](https://marketsquare.github.io/robotframework-dashboard/dashboard-server.html) - Host the dashboard for multi-user access, programmatic updates, and remote server integration.
82
83
  - 🗄️ [**Custom Database Class**](https://marketsquare.github.io/robotframework-dashboard/custom-database-class.html) - Extend or replace the default database backend to suit your storage needs, including SQLite, MySQL, or custom implementations.
83
84
  - 🔔 [**Listener Integration**](https://marketsquare.github.io/robotframework-dashboard/listener-integration.html) - Use a listener to automatically push test results to the dashboard for every executed run, integrating seamlessly into CI/CD pipelines.
@@ -122,4 +123,4 @@ This project is licensed under the MIT License.
122
123
  ## 🎬 RoboCon Talk
123
124
  See also my 2025 [RoboCon](https://robocon.io) talk, where I explain robotdashboard and its use cases. Note that the UI has improved significantly since then.
124
125
 
125
- [![Robocon 2025 talk](https://img.youtube.com/vi/olf1_pd9YfM/0.jpg)](https://www.youtube.com/watch?v=olf1_pd9YfM)
126
+ [![Robocon 2025 talk](https://img.youtube.com/vi/olf1_pd9YfM/0.jpg)](https://www.youtube.com/watch?v=olf1_pd9YfM)
@@ -4,8 +4,16 @@ from sys import exit
4
4
  from re import split
5
5
  from os import getcwd
6
6
  from os.path import join, exists
7
+ from typing import NamedTuple
7
8
  from .version import __version__
8
9
 
10
+ _VALID_LOG_TYPES = {"run", "suite", "test", "keyword", "all"}
11
+
12
+
13
+ class LogRemovedConfig(NamedTuple):
14
+ types: list
15
+ path: str
16
+
9
17
 
10
18
  class dotdict(dict):
11
19
  """dot.notation access to dictionary attributes"""
@@ -61,6 +69,47 @@ class ArgumentParser:
61
69
  print(" ERROR: Mixing --projectversion and version_ tags not supported")
62
70
  exit(2)
63
71
 
72
+ def _process_remove_runs(self, parts):
73
+ """Process and validate the comma-separated --removeruns parts.
74
+
75
+ Detects a scoped-retention combination ('limit' + 'tags') and rewrites
76
+ it into the internal 'limit=N;tag=...' form so the limit acts on the
77
+ tagged subset instead of running as two independent operations. This
78
+ lets users write the combo with the regular comma separator (e.g.
79
+ '-r "limit=10,tag=nightly"') instead of ';'.
80
+
81
+ Scope note (matches issue #309): only 'limit' can be scoped by tags.
82
+ 'age' + 'tags' is intentionally NOT supported here — combining them
83
+ just runs as two independent operations, same as on main.
84
+
85
+ Rules:
86
+ - No tags, or tags without a 'limit' partner -> no combination, every
87
+ part stays independent (unchanged behavior, including 'age'+'tags').
88
+ - Only one 'limit' may be combined with tags (error otherwise).
89
+ """
90
+ limit_parts = [p for p in parts if p.startswith("limit=")]
91
+ tag_parts = [p for p in parts if p.startswith("tag=")]
92
+ other_parts = [
93
+ p for p in parts if not (p.startswith("limit=") or p.startswith("tag="))
94
+ ]
95
+
96
+ if not tag_parts or not limit_parts:
97
+ return parts
98
+ if len(limit_parts) > 1:
99
+ print(" ERROR: Only one 'limit' may be combined with 'tag(s)'.")
100
+ exit(3)
101
+
102
+ tag_values = [p.replace("tag=", "") for p in tag_parts]
103
+ limit_value = limit_parts[0].replace("limit=", "")
104
+ combo = limit_parts[0] + "".join(f";tag={value}" for value in tag_values)
105
+ print(
106
+ f" INFO: Combining 'limit' with 'tag(s)' -> keeping the {limit_value} most recent "
107
+ f"run(s) tagged with [{', '.join(tag_values)}] and removing older matching runs "
108
+ f"(runs without these tags are left untouched)."
109
+ )
110
+ # other independent options (index/run_start/alias) run before the combo
111
+ return other_parts + [combo]
112
+
64
113
  def _check_argument_warnings(self, arguments, outputs, outputfolderpaths, use_logs, generate_dashboard, no_autoupdate, offline_dependencies):
65
114
  """Checks for argument combinations that are valid but likely unintended and prints warnings"""
66
115
  no_outputs = not outputs and not outputfolderpaths
@@ -267,6 +316,7 @@ class ArgumentParser:
267
316
  " • '-r run_start=2024-07-30 15:27:20.184407' -> remove specified run\n"
268
317
  " • '-r alias=some_alias,tag=prod'\n"
269
318
  " • '-r limit=10' -> keep only the 10 most recent runs\n"
319
+ " • '-r limit=10,tag=nightly' -> keep 10 newest 'nightly' runs, leave others\n"
270
320
  " • '-r age=10d' -> remove runs older than 10 days\n"
271
321
  " • '-r age=-10d' -> remove runs younger than 10 days\n"
272
322
  " • (y)ear/(d)ay/(h)our/(m)inute/(s)econd supported\n"
@@ -275,6 +325,24 @@ class ArgumentParser:
275
325
  nargs="*",
276
326
  default=None,
277
327
  )
328
+ db_group.add_argument(
329
+ "--logremoved",
330
+ metavar="TYPES_PATH",
331
+ help=(
332
+ "Log (append if file exists) run data to jsonl before removing from the db\n"
333
+ " • Supply what types to log (default=all) and (optional) a path to the desired .jsonl\n"
334
+ "Examples:\n"
335
+ " • '--logremoved \"/tmp/removed_runs.jsonl\"'\n"
336
+ " • '--logremoved \"run:suite\"'\n"
337
+ " • '--logremoved \"all:/tmp/removed_runs.jsonl\"'\n"
338
+ ),
339
+ action="store",
340
+ type=str,
341
+ nargs="?",
342
+ const="all",
343
+ default=None,
344
+ dest="logremoved",
345
+ )
278
346
  db_group.add_argument(
279
347
  "-c",
280
348
  "--databaseclass",
@@ -505,11 +573,13 @@ class ArgumentParser:
505
573
  # handles the processing of --removeruns
506
574
  remove_runs = None
507
575
  if arguments.removeruns:
508
- remove_runs = []
576
+ raw_parts = []
509
577
  for runs in arguments.removeruns:
510
- parts = str(runs[0]).split(",")
511
- for part in parts:
512
- remove_runs.append(part)
578
+ for part in str(runs[0]).split(","):
579
+ part = part.strip()
580
+ if part:
581
+ raw_parts.append(part)
582
+ remove_runs = self._process_remove_runs(raw_parts)
513
583
 
514
584
  # handles the boolean handling of relevant arguments
515
585
  generate_dashboard = self._normalize_bool(
@@ -605,6 +675,23 @@ class ArgumentParser:
605
675
  ssl_certfile = arguments.ssl_certfile
606
676
  ssl_keyfile = arguments.ssl_keyfile
607
677
 
678
+ # handles --logremoved: "type1:type2:path" or "path" or "type1:type2"
679
+ log_removed = None
680
+ if arguments.logremoved:
681
+ parts = arguments.logremoved.split(":")
682
+ types, remaining = [], list(parts)
683
+ # grab element types, leave path, for windows drive letter X: parse
684
+ for part in parts:
685
+ if part in _VALID_LOG_TYPES:
686
+ types.append(part)
687
+ remaining.pop(0)
688
+ else:
689
+ break
690
+ if not types:
691
+ types = ["all"]
692
+ path = ":".join(remaining) if remaining else "robot_removed_runs.jsonl"
693
+ log_removed = LogRemovedConfig(types=types, path=path)
694
+
608
695
  # validates argument combinations
609
696
  self._check_argument_errors(arguments, outputs, outputfolderpaths, force_json_config, database_class)
610
697
  self._check_argument_warnings(arguments, outputs, outputfolderpaths, use_logs, generate_dashboard, no_autoupdate, offline_dependencies)
@@ -640,5 +727,6 @@ class ArgumentParser:
640
727
  "ssl_certfile": ssl_certfile,
641
728
  "ssl_keyfile": ssl_keyfile,
642
729
  "log_url": arguments.logurl,
730
+ "log_removed": log_removed,
643
731
  }
644
732
  return dotdict(provided_args)
@@ -124,6 +124,14 @@ body {
124
124
  .boxplot-graph:hover svg,
125
125
  .shown-graph:hover svg,
126
126
  .hidden-graph:hover svg,
127
+ .move-to-first-graph:hover svg,
128
+ .move-to-last-graph:hover svg,
129
+ .add-stat-widget-header:hover svg,
130
+ .add-link-widget-header:hover svg,
131
+ .add-section-header:hover svg,
132
+ .delete-custom-stat-widget:hover svg,
133
+ .delete-custom-link-widget:hover svg,
134
+ .delete-custom-section:hover svg,
127
135
  .shown-section:hover svg,
128
136
  .hidden-section:hover svg,
129
137
  .collapse-icon:hover svg,
@@ -188,6 +196,14 @@ body.lock-scroll {
188
196
  .boxplot-graph,
189
197
  .shown-graph,
190
198
  .hidden-graph,
199
+ .move-to-first-graph,
200
+ .move-to-last-graph,
201
+ .add-stat-widget-header,
202
+ .add-link-widget-header,
203
+ .add-section-header,
204
+ .delete-custom-stat-widget,
205
+ .delete-custom-link-widget,
206
+ .delete-custom-section,
191
207
  .shown-section,
192
208
  .hidden-section,
193
209
  .move-up-table,
@@ -152,80 +152,44 @@
152
152
  align-items: center;
153
153
  }
154
154
 
155
- /* Add Widget tile shown in customize-view mode */
156
- .add-stat-widget-tile {
157
- cursor: pointer;
158
- display: flex;
159
- align-items: center;
160
- justify-content: center;
161
- border: 2px dashed var(--bs-border-color, rgba(128,128,128,0.4));
162
- border-radius: 8px;
163
- transition: border-color 0.15s, background-color 0.15s;
155
+ /* Color picker buttons inside the Add Stat Widget modal */
156
+ .stat-color-btn {
157
+ min-width: 60px;
164
158
  }
165
159
 
166
- .add-stat-widget-tile:hover {
167
- border-color: var(--color-text);
168
- background-color: rgba(128,128,128,0.08);
160
+ /* "All" tab stat list inside the Add Stat Widget modal */
161
+ .add-all-widgets-list {
162
+ max-height: 60vh;
163
+ overflow-y: auto;
169
164
  }
170
165
 
171
- .add-stat-widget-tile-inner {
166
+ .add-all-widgets-row {
172
167
  display: flex;
173
- flex-direction: column;
174
168
  align-items: center;
175
- gap: 0.25rem;
176
- transition: opacity 0.15s;
177
- user-select: none;
178
- }
179
-
180
- .add-stat-widget-plus {
181
- font-size: 2rem;
182
- font-weight: 300;
183
- line-height: 1;
184
- }
185
-
186
- .add-stat-widget-tile-label {
187
- font-size: 0.8rem;
188
- font-weight: 500;
189
- }
190
-
191
- /* Color picker buttons inside the Add Stat Widget modal */
192
- .stat-color-btn {
193
- min-width: 60px;
169
+ gap: 0.5rem;
170
+ margin-bottom: 0.5rem;
194
171
  }
195
172
 
196
- /* Add custom section horizontal tile — shown in customize-view mode (unified grid only) */
197
- .add-section-tile {
198
- cursor: pointer;
199
- display: flex;
200
- align-items: center;
201
- justify-content: center;
202
- border: 2px dashed var(--bs-border-color, rgba(128,128,128,0.4));
203
- border-radius: 8px;
204
- transition: border-color 0.15s, background-color 0.15s;
173
+ .add-all-widgets-row .form-check {
174
+ margin-bottom: 0;
205
175
  }
206
176
 
207
- .add-section-tile:hover {
208
- border-color: var(--color-text);
209
- background-color: rgba(128,128,128,0.08);
177
+ .add-all-widgets-row .add-all-widgets-title {
178
+ flex: 1 1 auto;
210
179
  }
211
180
 
212
- .add-section-tile-inner {
213
- display: flex;
214
- flex-direction: row;
215
- align-items: center;
216
- gap: 0.5rem;
217
- user-select: none;
181
+ .add-all-widgets-row .add-all-widgets-badge {
182
+ flex: 0 0 auto;
218
183
  }
219
184
 
220
- .add-section-plus {
221
- font-size: 1.5rem;
222
- font-weight: 300;
223
- line-height: 1;
185
+ .add-all-widgets-group-heading {
186
+ font-weight: 600;
187
+ margin-top: 0.5rem;
188
+ margin-bottom: 0.25rem;
224
189
  }
225
190
 
226
- .add-section-tile-label {
227
- font-size: 0.85rem;
228
- font-weight: 500;
191
+ .add-all-widgets-group-heading:first-child {
192
+ margin-top: 0;
229
193
  }
230
194
 
231
195
  /* Custom section divider bar rendered inside the unified GridStack */
@@ -247,20 +211,16 @@
247
211
  user-select: none;
248
212
  }
249
213
 
250
- /* Delete button on custom section dividers — pinned to top-right */
251
- .delete-custom-section {
252
- position: absolute;
253
- top: 5px;
254
- right: 5px;
255
- z-index: 1;
256
- }
257
-
258
- /* Delete button on custom stat widgets — pinned to top-right, out of flex flow */
259
- .delete-custom-stat-widget {
214
+ /* Move to first/last + delete controls on custom widgets/sections — pinned to top-right,
215
+ out of flex flow, sized and spaced like the icons in graph headers */
216
+ .custom-widget-move-controls {
260
217
  position: absolute;
261
218
  top: 5px;
262
219
  right: 5px;
263
220
  z-index: 1;
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 8px;
264
224
  }
265
225
 
266
226
  /* Compact flex layout for custom stat widgets — works at any GridStack size */
@@ -284,7 +244,12 @@
284
244
  overflow: hidden;
285
245
  text-overflow: ellipsis;
286
246
  white-space: nowrap;
287
- max-width: calc(100% - 20px); /* leave room so text doesn't run under the delete button */
247
+ max-width: 100%;
248
+ }
249
+
250
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
251
+ .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title {
252
+ max-width: calc(100% - 96px);
288
253
  }
289
254
 
290
255
  /* When the item is very short (h=1 row ≈ 60px) switch to a side-by-side row */
@@ -304,14 +269,25 @@
304
269
  .grid-stack-item[data-gs-h="1"] .stat-widget-title {
305
270
  flex: 1 1 0;
306
271
  text-align: left;
307
- max-width: calc(100% - 40px); /* value + delete button */
272
+ max-width: calc(100% - 40px); /* leave room for the value */
273
+ }
274
+
275
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
276
+ .grid-stack-item[gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title,
277
+ .grid-stack-item[data-gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title {
278
+ max-width: calc(100% - 136px); /* value + move/delete controls */
308
279
  }
309
280
 
310
281
  .grid-stack-item[gs-h="1"] .stat-value,
311
282
  .grid-stack-item[data-gs-h="1"] .stat-value {
312
283
  flex-shrink: 0;
313
284
  font-size: 1.1rem;
314
- margin-right: 20px; /* keep clear of the absolute delete button */
285
+ }
286
+
287
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
288
+ .grid-stack-item[gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-value,
289
+ .grid-stack-item[data-gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-value {
290
+ margin-right: 96px;
315
291
  }
316
292
 
317
293
  .grid-stack-item-content.blue-bg { background-color: rgba(13, 110, 253, 0.15); }
@@ -319,6 +295,92 @@
319
295
  .grid-stack-item-content.red-bg { background-color: rgba(206, 62, 1, 0.2); }
320
296
  .grid-stack-item-content.yellow-bg { background-color: rgba(254, 216, 79, 0.2); }
321
297
 
298
+ /* ─── Custom link widget ─────────────────────────────────────────────────── */
299
+
300
+ /* Inner layout — mirrors stat-widget-inner */
301
+ .link-widget-inner {
302
+ position: relative;
303
+ display: flex;
304
+ flex-direction: column;
305
+ justify-content: center;
306
+ align-items: center;
307
+ gap: 0.25rem;
308
+ height: 100%;
309
+ text-align: center;
310
+ overflow: hidden;
311
+ }
312
+
313
+ /* Label — main large text, like .stat-value */
314
+ .link-widget-label {
315
+ margin: 0;
316
+ font-weight: 600;
317
+ font-size: 1.5rem;
318
+ line-height: 1.2;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ white-space: nowrap;
322
+ max-width: 100%;
323
+ }
324
+
325
+ /* URL preview — small muted text below the label */
326
+ .link-widget-url {
327
+ font-size: 0.72rem;
328
+ opacity: 0.55;
329
+ overflow: hidden;
330
+ text-overflow: ellipsis;
331
+ white-space: nowrap;
332
+ max-width: 100%;
333
+ }
334
+
335
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
336
+ .link-widget-inner:has(.custom-widget-move-controls) .link-widget-label,
337
+ .link-widget-inner:has(.custom-widget-move-controls) .link-widget-url {
338
+ max-width: calc(100% - 96px);
339
+ }
340
+
341
+ /* Clickable in non-edit mode */
342
+ .link-widget-clickable {
343
+ cursor: pointer;
344
+ }
345
+
346
+ /* Same padding as stat widget tiles */
347
+ .grid-stack-item-content:has(.link-widget-inner) {
348
+ padding: 8px 12px;
349
+ }
350
+
351
+ /* h=1 compact row — mirrors stat widget compact layout */
352
+ .grid-stack-item[gs-h="1"] .link-widget-inner,
353
+ .grid-stack-item[data-gs-h="1"] .link-widget-inner {
354
+ flex-direction: row;
355
+ justify-content: space-between;
356
+ align-items: center;
357
+ gap: 0.5rem;
358
+ }
359
+
360
+ .grid-stack-item[gs-h="1"] .link-widget-label,
361
+ .grid-stack-item[data-gs-h="1"] .link-widget-label {
362
+ flex: 1 1 0;
363
+ font-size: 1.1rem;
364
+ text-align: left;
365
+ max-width: calc(60% - 10px);
366
+ }
367
+
368
+ .grid-stack-item[gs-h="1"] .link-widget-url,
369
+ .grid-stack-item[data-gs-h="1"] .link-widget-url {
370
+ flex-shrink: 0;
371
+ font-size: 0.72rem;
372
+ text-align: right;
373
+ max-width: 40%;
374
+ }
375
+
376
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
377
+ .grid-stack-item[gs-h="1"] .link-widget-inner:has(.custom-widget-move-controls) .link-widget-url {
378
+ max-width: calc(40% - 96px);
379
+ margin-right: 96px;
380
+ }
381
+
382
+ /* ─────────────────────────────────────────────────────────────────────────── */
383
+
322
384
  .fullscreen .stat-value {
323
385
  font-size: 4rem;
324
386
  }
@@ -361,6 +423,22 @@
361
423
  height: 200px;
362
424
  }
363
425
 
426
+ /* relative run time, pinned to the bottom right corner of a run card without
427
+ affecting the layout of the stats/duration columns next to the donut */
428
+ .overview-card .card-body {
429
+ position: relative;
430
+ }
431
+
432
+ .run-card-run-time {
433
+ position: absolute;
434
+ right: 1rem;
435
+ bottom: 0.55rem;
436
+ color: var(--color-text-muted);
437
+ cursor: help;
438
+ font-size: 0.85em;
439
+ white-space: nowrap;
440
+ }
441
+
364
442
  .overview-card {
365
443
  cursor: pointer;
366
444
  min-width: 300px;
@@ -425,6 +503,13 @@
425
503
  gap: 8px;
426
504
  }
427
505
 
506
+ /* Icon row in section card headers (add stat/link widget, add section, move/show/hide section) */
507
+ .section-header-icons {
508
+ display: flex;
509
+ align-items: center;
510
+ gap: 8px;
511
+ }
512
+
428
513
  .grid-stack-item-content .graph-body {
429
514
  flex: 1 1 auto;
430
515
  display: flex;
@@ -1,5 +1,4 @@
1
1
  .modal-dialog {
2
- background: var(--color-modal-bg);
3
2
  color: var(--color-text);
4
3
  }
5
4