rushti 2.2.3__tar.gz → 2.3.1__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 (63) hide show
  1. {rushti-2.2.3/src/rushti.egg-info → rushti-2.3.1}/PKG-INFO +6 -1
  2. {rushti-2.2.3 → rushti-2.3.1}/README.md +5 -0
  3. {rushti-2.2.3 → rushti-2.3.1}/pyproject.toml +1 -1
  4. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/__init__.py +1 -1
  5. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/app_paths.py +22 -0
  6. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/cli.py +22 -4
  7. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/build.py +12 -6
  8. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/resume.py +5 -0
  9. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/__init__.py +12 -3
  10. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/dashboard.py +61 -17
  11. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/execution.py +222 -16
  12. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/logging.py +17 -6
  13. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/messages.py +25 -0
  14. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/optimizer.py +4 -2
  15. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/parsing.py +19 -8
  16. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/dynamodb.py +9 -2
  17. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/repository.py +1 -0
  18. rushti-2.3.1/src/rushti/stats/signature.py +42 -0
  19. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/sqlite.py +29 -11
  20. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/task.py +66 -14
  21. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/taskfile.py +87 -10
  22. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/taskfile_ops.py +21 -9
  23. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/templates/visualization.html +20 -5
  24. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_integration.py +26 -7
  25. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_objects.py +10 -0
  26. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/visualization_template.py +20 -5
  27. {rushti-2.2.3 → rushti-2.3.1/src/rushti.egg-info}/PKG-INFO +6 -1
  28. rushti-2.2.3/src/rushti/stats/signature.py +0 -29
  29. {rushti-2.2.3 → rushti-2.3.1}/LICENSE +0 -0
  30. {rushti-2.2.3 → rushti-2.3.1}/setup.cfg +0 -0
  31. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/_shlex_utils.py +0 -0
  32. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/checkpoint.py +0 -0
  33. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/__init__.py +0 -0
  34. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/db.py +0 -0
  35. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/__init__.py +0 -0
  36. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/analyze.py +0 -0
  37. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/export.py +0 -0
  38. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/list.py +0 -0
  39. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/optimize.py +0 -0
  40. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/visualize.py +0 -0
  41. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/expand.py +0 -0
  42. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/export.py +0 -0
  43. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/push.py +0 -0
  44. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/validate.py +0 -0
  45. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/visualize.py +0 -0
  46. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/contention_analyzer.py +0 -0
  47. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/dag.py +0 -0
  48. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/db_admin.py +0 -0
  49. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/exclusive.py +0 -0
  50. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/logging_setup.py +0 -0
  51. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/optimization_report.py +0 -0
  52. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/results_writer.py +0 -0
  53. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/settings.py +0 -0
  54. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/__init__.py +0 -0
  55. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/paths.py +0 -0
  56. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/templates/__init__.py +0 -0
  57. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_build.py +0 -0
  58. {rushti-2.2.3 → rushti-2.3.1}/src/rushti/utils.py +0 -0
  59. {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/SOURCES.txt +0 -0
  60. {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/dependency_links.txt +0 -0
  61. {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/entry_points.txt +0 -0
  62. {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/requires.txt +0 -0
  63. {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rushti
3
- Version: 2.2.3
3
+ Version: 2.3.1
4
4
  Summary: RushTI - Parallel TM1 TurboIntegrator Process Execution
5
5
  Author-email: nicolasbisurgi <nbisurgi@cubewise.com>, Marius Wirtz <mwirtz@cubewise.com>
6
6
  Maintainer: Cubewise CODE
@@ -109,6 +109,11 @@ user = admin
109
109
  password = apple
110
110
  ```
111
111
 
112
+ RushTI looks for `config.ini` under `config/` (or `RUSHTI_DIR`). To point a
113
+ single run at a different file — e.g. a `config.ini` shared with other tm1py
114
+ utilities — pass `--config PATH` to any TM1-connecting command (`run`, `build`,
115
+ `tasks`, `resume`).
116
+
112
117
  **2. Create a task file**
113
118
 
114
119
  ```json
@@ -65,6 +65,11 @@ user = admin
65
65
  password = apple
66
66
  ```
67
67
 
68
+ RushTI looks for `config.ini` under `config/` (or `RUSHTI_DIR`). To point a
69
+ single run at a different file — e.g. a `config.ini` shared with other tm1py
70
+ utilities — pass `--config PATH` to any TM1-connecting command (`run`, `build`,
71
+ `tasks`, `resume`).
72
+
68
73
  **2. Create a task file**
69
74
 
70
75
  ```json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rushti"
7
- version = "2.2.3"
7
+ version = "2.3.1"
8
8
  description = "RushTI - Parallel TM1 TurboIntegrator Process Execution"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ This package provides tools for executing TM1 TI processes in parallel
5
5
  with dependency management, checkpoint/resume support, and execution logging.
6
6
  """
7
7
 
8
- __version__ = "2.2.3"
8
+ __version__ = "2.3.1"
9
9
  __app_name__ = "RushTI"
10
10
 
11
11
  # Core exports for programmatic use
@@ -6,6 +6,7 @@ and tracks which legacy paths have been used so a deprecation warning
6
6
  can be emitted once logging is initialized.
7
7
  """
8
8
 
9
+ import argparse
9
10
  import logging
10
11
  import os
11
12
 
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "CURRENT_DIRECTORY",
16
17
  "resolve_config_path",
17
18
  "log_legacy_path_warnings",
19
+ "add_config_arg",
18
20
  ]
19
21
 
20
22
 
@@ -73,6 +75,26 @@ def resolve_config_path(filename: str, warn_on_legacy: bool = True, cli_path: st
73
75
  return new_path
74
76
 
75
77
 
78
+ def add_config_arg(parser: argparse.ArgumentParser) -> None:
79
+ """Add the ``--config`` argument to a parser.
80
+
81
+ Long-form only (``-c`` is taken by ``resume --checkpoint``). Relocates
82
+ only ``config.ini`` (TM1 connection parameters); ``settings.ini`` and
83
+ ``logging_config.ini`` keep their own resolution. The resolved value is
84
+ fed to :func:`resolve_config_path` as ``cli_path``.
85
+ """
86
+ parser.add_argument(
87
+ "--config",
88
+ dest="config",
89
+ default=None,
90
+ metavar="FILE",
91
+ help=(
92
+ "Path to a config.ini file (TM1 connection parameters). "
93
+ "Overrides RUSHTI_DIR/default location."
94
+ ),
95
+ )
96
+
97
+
76
98
  def log_legacy_path_warnings(logger: logging.Logger) -> None:
77
99
  """Log deprecation warnings for any legacy paths that were used.
78
100
 
@@ -19,6 +19,7 @@ from pathlib import Path
19
19
  from TM1py.Utils import integerize_version
20
20
 
21
21
  from rushti.app_paths import (
22
+ add_config_arg,
22
23
  log_legacy_path_warnings,
23
24
  resolve_config_path,
24
25
  )
@@ -249,11 +250,14 @@ Examples:
249
250
  %(prog)s -t tasks.txt -w 4 -r 2 -o results.csv
250
251
  %(prog)s --tasks tasks.json --max-workers 8 --retries 3
251
252
  %(prog)s --tm1-instance tm1srv01 --workflow Sample --max-workers 4
253
+ %(prog)s --tm1-instance tm1srv01 --workflow Sample_Optimal_Mode --mode opt
252
254
 
253
255
  Configuration:
254
256
  Default values can be set in settings.ini (see settings.ini.template).
255
257
  Settings precedence: CLI args > JSON task file > settings.ini > defaults
256
- File format is auto-detected (--mode is deprecated for file sources).
258
+ File sources auto-detect the mode (--mode is ignored). Cube reads
259
+ (--tm1-instance) cannot, and default to norm; pass --mode opt to honour
260
+ the 'predecessors' measure.
257
261
  """,
258
262
  )
259
263
 
@@ -270,6 +274,9 @@ Configuration:
270
274
  # Add taskfile source arguments
271
275
  add_taskfile_source_args(parser, required=False, include_settings=True)
272
276
 
277
+ # Add --config (relocates config.ini only)
278
+ add_config_arg(parser)
279
+
273
280
  parser.add_argument(
274
281
  "--max-workers",
275
282
  "-w",
@@ -286,7 +293,7 @@ Configuration:
286
293
  dest="execution_mode",
287
294
  choices=["norm", "opt"],
288
295
  default=None,
289
- help="[Deprecated] Execution mode is now auto-detected from file content. This option is kept for backwards compatibility but ignored.",
296
+ help="Execution mode: norm or opt. Auto-detected (and ignored) for file sources. For cube reads (--tm1-instance) it defaults to norm; pass 'opt' to honour the 'predecessors' measure.",
290
297
  )
291
298
 
292
299
  parser.add_argument(
@@ -423,6 +430,7 @@ def parse_named_arguments(argv: list):
423
430
  "workflow": workflow,
424
431
  "log_level": args.log_level,
425
432
  "detailed_results": args.detailed_results,
433
+ "config": args.config,
426
434
  }
427
435
 
428
436
  return tasks_file_path, cli_args
@@ -468,6 +476,7 @@ def parse_arguments(argv: list):
468
476
  "exclusive": None,
469
477
  "no_checkpoint": False,
470
478
  "log_level": None, # Not supported in positional style
479
+ "config": None, # Legacy positional style does not support --config
471
480
  }
472
481
  return tasks_file, cli_args
473
482
 
@@ -623,6 +632,14 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
623
632
  if resume_context:
624
633
  cli_args.update(resume_context)
625
634
 
635
+ # Resolve config.ini location, threading the resolved path explicitly into
636
+ # the TM1 connection layer (CLI --config > RUSHTI_DIR > legacy CWD > config/).
637
+ # Fail fast with a clean message (no traceback) on an explicit bad path.
638
+ try:
639
+ config_path = resolve_config_path("config.ini", cli_path=cli_args.get("config"))
640
+ except FileNotFoundError:
641
+ sys.exit(f"RushTI: --config file not found: {cli_args.get('config')}")
642
+
626
643
  # Apply log level override if specified
627
644
  apply_log_level(cli_args.get("log_level"))
628
645
 
@@ -642,7 +659,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
642
659
  try:
643
660
  from rushti.tm1_integration import read_taskfile_from_tm1, connect_to_tm1_instance
644
661
 
645
- tm1_source = connect_to_tm1_instance(tm1_instance, CONFIG)
662
+ tm1_source = connect_to_tm1_instance(tm1_instance, config_path)
646
663
  try:
647
664
  # Resolve execution mode for TM1 read (affects predecessor parsing)
648
665
  tm1_mode = "opt" if cli_args.get("execution_mode") == ExecutionMode.OPT else "norm"
@@ -749,6 +766,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
749
766
  tasks_file_path=tasks_file_path_for_services if not tm1_taskfile else None,
750
767
  workflow=workflow,
751
768
  exclusive=exclusive_mode,
769
+ config_path=config_path,
752
770
  tm1_instances=tm1_instances_needed,
753
771
  )
754
772
 
@@ -1103,7 +1121,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
1103
1121
  )
1104
1122
  tm1_upload = connect_to_tm1_instance(
1105
1123
  upload_instance,
1106
- CONFIG,
1124
+ config_path,
1107
1125
  )
1108
1126
  try:
1109
1127
  results_df = build_results_dataframe(
@@ -10,6 +10,7 @@ import sys
10
10
 
11
11
  from TM1py import TM1Service
12
12
 
13
+ from rushti.app_paths import add_config_arg, resolve_config_path
13
14
  from rushti.logging_setup import add_log_level_arg, apply_log_level
14
15
  from rushti.settings import load_settings
15
16
  from rushti.tm1_build import build_logging_objects, get_build_status
@@ -25,8 +26,6 @@ def run_build_command(argv: list) -> None:
25
26
 
26
27
  :param argv: Command line arguments
27
28
  """
28
- from rushti.cli import CONFIG
29
-
30
29
  parser = argparse.ArgumentParser(
31
30
  prog=f"{APP_NAME} build",
32
31
  description="Create TM1 dimensions and cube for RushTI execution logging.",
@@ -60,6 +59,7 @@ Examples:
60
59
  metavar="FILE",
61
60
  help="Path to settings.ini file",
62
61
  )
62
+ add_config_arg(parser)
63
63
  add_log_level_arg(parser)
64
64
 
65
65
  args = parser.parse_args(argv[2:]) # Skip "rushti" and "build"
@@ -68,16 +68,22 @@ Examples:
68
68
  # Load settings
69
69
  settings = load_settings(args.settings_file)
70
70
 
71
+ # Resolve config.ini location (CLI --config > RUSHTI_DIR > legacy CWD > config/)
72
+ try:
73
+ config_path = resolve_config_path("config.ini", cli_path=args.config)
74
+ except FileNotFoundError:
75
+ sys.exit(f"RushTI: --config file not found: {args.config}")
76
+
71
77
  # Load config to get TM1 connection details
72
- if not os.path.isfile(CONFIG):
73
- print(f"Error: {CONFIG} does not exist")
78
+ if not os.path.isfile(config_path):
79
+ print(f"Error: {config_path} does not exist")
74
80
  sys.exit(1)
75
81
 
76
82
  config = configparser.ConfigParser()
77
- config.read(CONFIG, encoding="utf-8")
83
+ config.read(config_path, encoding="utf-8")
78
84
 
79
85
  if args.tm1_instance not in config.sections():
80
- print(f"Error: Instance '{args.tm1_instance}' not found in {CONFIG}")
86
+ print(f"Error: Instance '{args.tm1_instance}' not found in {config_path}")
81
87
  print(f"Available instances: {', '.join(config.sections())}")
82
88
  sys.exit(1)
83
89
 
@@ -10,6 +10,7 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import Optional
12
12
 
13
+ from rushti.app_paths import add_config_arg
13
14
  from rushti.checkpoint import find_checkpoint_for_taskfile, load_checkpoint
14
15
  from rushti.logging_setup import add_log_level_arg, apply_log_level
15
16
  from rushti.settings import load_settings
@@ -86,6 +87,7 @@ Examples:
86
87
  default=False,
87
88
  help="Force resume even if checkpoint doesn't match taskfile",
88
89
  )
90
+ add_config_arg(parser)
89
91
  add_log_level_arg(parser)
90
92
 
91
93
  args = parser.parse_args(argv[2:])
@@ -200,6 +202,9 @@ Examples:
200
202
  if args.settings_file:
201
203
  resume_argv.extend(["--settings", args.settings_file])
202
204
 
205
+ if args.config:
206
+ resume_argv.extend(["--config", args.config])
207
+
203
208
  if args.force:
204
209
  resume_argv.append("--force")
205
210
 
@@ -9,8 +9,9 @@ Subcommands:
9
9
  """
10
10
 
11
11
  import argparse
12
+ import sys
12
13
 
13
- from rushti.app_paths import resolve_config_path
14
+ from rushti.app_paths import add_config_arg, resolve_config_path
14
15
  from rushti.commands.tasks.expand import handle_tasks_expand
15
16
  from rushti.commands.tasks.export import handle_tasks_export
16
17
  from rushti.commands.tasks.push import handle_tasks_push
@@ -98,6 +99,7 @@ Examples:
98
99
  help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
99
100
  "Ignored for file sources (auto-detected). Default: opt",
100
101
  )
102
+ add_config_arg(export_parser)
101
103
  add_log_level_arg(export_parser)
102
104
 
103
105
  # --- push subcommand ---
@@ -122,6 +124,7 @@ Examples:
122
124
  help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
123
125
  "Ignored for file sources (auto-detected). Default: opt",
124
126
  )
127
+ add_config_arg(push_parser)
125
128
  add_log_level_arg(push_parser)
126
129
 
127
130
  # --- expand subcommand ---
@@ -155,6 +158,7 @@ Examples:
155
158
  help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
156
159
  "Ignored for file sources (auto-detected). Default: opt",
157
160
  )
161
+ add_config_arg(expand_parser)
158
162
  add_log_level_arg(expand_parser)
159
163
 
160
164
  # --- visualize subcommand ---
@@ -188,6 +192,7 @@ Examples:
188
192
  help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
189
193
  "Ignored for file sources (auto-detected). Default: opt",
190
194
  )
195
+ add_config_arg(visualize_parser)
191
196
  add_log_level_arg(visualize_parser)
192
197
 
193
198
  # --- validate subcommand ---
@@ -219,14 +224,18 @@ Examples:
219
224
  help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
220
225
  "Ignored for file sources (auto-detected). Default: opt",
221
226
  )
227
+ add_config_arg(validate_parser)
222
228
  add_log_level_arg(validate_parser)
223
229
 
224
230
  # Parse arguments (skip script name and 'tasks' command)
225
231
  args = parser.parse_args(argv[2:])
226
232
  apply_log_level(args.log_level)
227
233
 
228
- # Resolve config path
229
- config_path = resolve_config_path("config.ini", cli_path=None)
234
+ # Resolve config.ini location (CLI --config > RUSHTI_DIR > legacy CWD > config/)
235
+ try:
236
+ config_path = resolve_config_path("config.ini", cli_path=args.config)
237
+ except FileNotFoundError:
238
+ sys.exit(f"RushTI: --config file not found: {args.config}")
230
239
 
231
240
  # Dispatch to appropriate handler based on subcommand
232
241
  if args.subcommand == "export":
@@ -234,16 +234,26 @@ def _prepare_dashboard_data(
234
234
  }
235
235
  )
236
236
 
237
- # Build per-task aggregate data (across all runs)
237
+ # Build per-task aggregate data (across all runs). Each row carries
238
+ # both ``process`` (may be empty for chore rows) and ``chore`` so the
239
+ # dashboard can render a unified "Task target" column with a kind
240
+ # indicator. ``task_target``/``task_kind`` are the canonical fields
241
+ # for display; the raw kind fields stay populated for backward
242
+ # compatibility with downstream consumers.
238
243
  task_data: Dict[str, Dict[str, Any]] = {}
239
244
  for tr in task_results:
240
245
  sig = tr["task_signature"]
246
+ process_val = tr.get("process") or ""
247
+ chore_val = tr.get("chore") or ""
241
248
  if sig not in task_data:
242
249
  task_data[sig] = {
243
250
  "task_signature": sig,
244
251
  "task_id": tr["task_id"],
245
252
  "instance": tr["instance"],
246
- "process": tr["process"],
253
+ "process": process_val,
254
+ "chore": chore_val,
255
+ "task_target": chore_val or process_val,
256
+ "task_kind": "chore" if chore_val else "process",
247
257
  "durations": [],
248
258
  "successes": 0,
249
259
  "total": 0,
@@ -263,6 +273,9 @@ def _prepare_dashboard_data(
263
273
  "task_id": data["task_id"],
264
274
  "instance": data["instance"],
265
275
  "process": data["process"],
276
+ "chore": data["chore"],
277
+ "task_target": data["task_target"],
278
+ "task_kind": data["task_kind"],
266
279
  "executions": data["total"],
267
280
  "success_rate": (
268
281
  round(data["successes"] / data["total"] * 100, 1) if data["total"] > 0 else 0
@@ -290,10 +303,15 @@ def _prepare_dashboard_data(
290
303
  outliers = []
291
304
  for tr in task_results:
292
305
  if tr["duration_seconds"] is not None:
306
+ process_val = tr.get("process") or ""
307
+ chore_val = tr.get("chore") or ""
293
308
  outliers.append(
294
309
  {
295
310
  "task_id": tr["task_id"],
296
- "process": tr["process"],
311
+ "process": process_val,
312
+ "chore": chore_val,
313
+ "task_target": chore_val or process_val,
314
+ "task_kind": "chore" if chore_val else "process",
297
315
  "instance": tr["instance"],
298
316
  "run_id": tr["run_id"],
299
317
  "duration": round(tr["duration_seconds"], 2),
@@ -308,10 +326,15 @@ def _prepare_dashboard_data(
308
326
  failures = []
309
327
  for tr in task_results:
310
328
  if tr["status"] != "Success":
329
+ process_val = tr.get("process") or ""
330
+ chore_val = tr.get("chore") or ""
311
331
  failures.append(
312
332
  {
313
333
  "task_id": tr["task_id"],
314
- "process": tr["process"],
334
+ "process": process_val,
335
+ "chore": chore_val,
336
+ "task_target": chore_val or process_val,
337
+ "task_kind": "chore" if chore_val else "process",
315
338
  "instance": tr["instance"],
316
339
  "run_id": tr["run_id"],
317
340
  "duration": round(tr["duration_seconds"], 2) if tr["duration_seconds"] else 0,
@@ -322,13 +345,18 @@ def _prepare_dashboard_data(
322
345
  # Slim task_results for JS (only fields needed for interactive filtering)
323
346
  slim_task_results = []
324
347
  for tr in task_results:
348
+ process_val = tr.get("process") or ""
349
+ chore_val = tr.get("chore") or ""
325
350
  slim_task_results.append(
326
351
  {
327
352
  "run_id": tr["run_id"],
328
353
  "task_id": tr["task_id"],
329
354
  "task_signature": tr["task_signature"],
330
355
  "instance": tr["instance"],
331
- "process": tr["process"],
356
+ "process": process_val,
357
+ "chore": chore_val,
358
+ "task_target": chore_val or process_val,
359
+ "task_kind": "chore" if chore_val else "process",
332
360
  "status": tr["status"],
333
361
  "duration_seconds": tr["duration_seconds"],
334
362
  "error_message": tr.get("error_message"),
@@ -578,6 +606,11 @@ def generate_dashboard(
578
606
  .status-partial {{ color: #D97706; font-weight: 600; }}
579
607
  .high-cv {{ background: #FFF7ED; }}
580
608
  .duration-slow {{ color: #DC2626; font-weight: 600; }}
609
+ .kind-tag {{
610
+ display: inline-block; padding: 1px 5px; border-radius: 4px;
611
+ font-size: 0.7rem; font-weight: 700; color: #475569;
612
+ background: #E2E8F0; margin-right: 4px;
613
+ }}
581
614
 
582
615
  /* Accordion */
583
616
  .accordion {{ margin-bottom: 20px; }}
@@ -752,7 +785,7 @@ def generate_dashboard(
752
785
  <thead><tr>
753
786
  <th onclick="sortTaskTable(0)">Task ID <span class="sort-arrow"></span></th>
754
787
  <th onclick="sortTaskTable(1)">Instance <span class="sort-arrow"></span></th>
755
- <th onclick="sortTaskTable(2)">Process <span class="sort-arrow"></span></th>
788
+ <th onclick="sortTaskTable(2)">Task target <span class="sort-arrow"></span></th>
756
789
  <th onclick="sortTaskTable(3)">Avg (s) <span class="sort-arrow"></span></th>
757
790
  <th onclick="sortTaskTable(4)">Min (s) <span class="sort-arrow"></span></th>
758
791
  <th onclick="sortTaskTable(5)">Max (s) <span class="sort-arrow"></span></th>
@@ -768,7 +801,7 @@ def generate_dashboard(
768
801
  <h3>Top 10 Slowest Executions <span class="help-icon">?<span class="help-tip">How to read: These are the 10 longest-running individual task executions across the selected runs. The "vs Median" column tells you how many times slower each one was compared to the typical task — a high multiplier (e.g., 5×) means that execution was significantly slower than usual and may be worth investigating. Check whether the same task appears multiple times — if so, it is a consistent bottleneck.</span></span></h3>
769
802
  <table id="outlierTable">
770
803
  <thead><tr>
771
- <th>Task ID</th><th>Process</th><th>Instance</th>
804
+ <th>Task ID</th><th>Task target</th><th>Instance</th>
772
805
  <th>Run</th><th>Duration (s)</th><th>vs Median</th><th>Status</th>
773
806
  </tr></thead>
774
807
  <tbody id="outlierTableBody"></tbody>
@@ -779,7 +812,7 @@ def generate_dashboard(
779
812
  <h3>Failed Processes <span class="help-icon">?<span class="help-tip">How to read: Every task execution that did not succeed is listed here. Look for patterns — does the same task fail repeatedly across runs? That points to a systemic issue. If failures are scattered across different tasks, the problem may be environmental (server load, connectivity). Hover over the Error column to see the full error message for each failure.</span></span></h3>
780
813
  <table id="failureTable">
781
814
  <thead><tr>
782
- <th>Task ID</th><th>Process</th><th>Instance</th>
815
+ <th>Task ID</th><th>Task target</th><th>Instance</th>
783
816
  <th>Run</th><th>Duration (s)</th><th>Error</th>
784
817
  </tr></thead>
785
818
  <tbody id="failureTableBody"></tbody>
@@ -1260,14 +1293,14 @@ def generate_dashboard(
1260
1293
  // Also store a lookup for tooltip labels
1261
1294
  const sigLabel = {{}};
1262
1295
  relevant.forEach(tr => {{
1263
- if (!sigLabel[tr.task_signature]) sigLabel[tr.task_signature] = tr.process;
1296
+ if (!sigLabel[tr.task_signature]) sigLabel[tr.task_signature] = tr.task_target || tr.process || tr.chore;
1264
1297
  }});
1265
1298
 
1266
1299
  const datasets = runs.map((run, idx) => {{
1267
1300
  const runTasks = relevant.filter(tr => tr.run_id === run.run_id);
1268
1301
  return {{
1269
1302
  label: formatDate(run.start_time),
1270
- data: runTasks.map(t => ({{ x: sigIndex[t.task_signature], y: t.duration_seconds || 0, _sig: t.task_signature, _task: t.task_id, _proc: t.process }})),
1303
+ data: runTasks.map(t => ({{ x: sigIndex[t.task_signature], y: t.duration_seconds || 0, _sig: t.task_signature, _task: t.task_id, _proc: t.task_target || t.process || t.chore }})),
1271
1304
  backgroundColor: RUN_COLORS[idx % RUN_COLORS.length] + '80',
1272
1305
  borderColor: RUN_COLORS[idx % RUN_COLORS.length],
1273
1306
  pointRadius: 3,
@@ -1470,7 +1503,10 @@ def generate_dashboard(
1470
1503
  const sig = tr.task_signature;
1471
1504
  if (!taskData[sig]) {{
1472
1505
  taskData[sig] = {{
1473
- task_id: tr.task_id, instance: tr.instance, process: tr.process,
1506
+ task_id: tr.task_id, instance: tr.instance,
1507
+ process: tr.process, chore: tr.chore,
1508
+ task_target: tr.task_target || tr.process || tr.chore,
1509
+ task_kind: tr.task_kind || (tr.chore ? 'chore' : 'process'),
1474
1510
  durations: [], successes: 0, total: 0
1475
1511
  }};
1476
1512
  }}
@@ -1492,7 +1528,7 @@ def generate_dashboard(
1492
1528
  }}
1493
1529
 
1494
1530
  function applyTaskSort(rows) {{
1495
- const fields = ['task_id', 'instance', 'process', 'avg', 'mn', 'mx', 'stdDev', 'successRate', 'total'];
1531
+ const fields = ['task_id', 'instance', 'task_target', 'avg', 'mn', 'mx', 'stdDev', 'successRate', 'total'];
1496
1532
  const field = fields[taskTableSortCol];
1497
1533
  rows.sort((a, b) => {{
1498
1534
  const av = a[field], bv = b[field];
@@ -1556,10 +1592,12 @@ def generate_dashboard(
1556
1592
  tbody.innerHTML = pageRows.map(r => {{
1557
1593
  const cvClass = r.cv > 0.5 ? 'high-cv' : '';
1558
1594
  const statusCls = r.successRate < 100 ? 'status-fail' : 'status-success';
1595
+ const kindTag = r.task_kind === 'chore' ? '[C]' : '[P]';
1596
+ const target = r.task_target || r.process || r.chore || '';
1559
1597
  return `<tr class="${{cvClass}}">
1560
1598
  <td class="mono">${{r.task_id}}</td>
1561
1599
  <td>${{r.instance}}</td>
1562
- <td>${{r.process}}</td>
1600
+ <td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
1563
1601
  <td class="mono">${{r.avg.toFixed(2)}}</td>
1564
1602
  <td class="mono">${{r.mn.toFixed(2)}}</td>
1565
1603
  <td class="mono">${{r.mx.toFixed(2)}}</td>
@@ -1585,9 +1623,11 @@ def generate_dashboard(
1585
1623
  tbody.innerHTML = sorted.map(o => {{
1586
1624
  const statusCls = o.status === 'Success' ? 'status-success' : 'status-fail';
1587
1625
  const vsMedian = o.duration_seconds - median;
1626
+ const kindTag = o.task_kind === 'chore' || o.chore ? '[C]' : '[P]';
1627
+ const target = o.task_target || o.process || o.chore || '';
1588
1628
  return `<tr>
1589
1629
  <td class="mono">${{o.task_id}}</td>
1590
- <td>${{o.process}}</td>
1630
+ <td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
1591
1631
  <td>${{o.instance}}</td>
1592
1632
  <td class="mono">${{o.run_id}}</td>
1593
1633
  <td class="mono duration-slow">${{o.duration_seconds.toFixed(2)}}</td>
@@ -1613,15 +1653,19 @@ def generate_dashboard(
1613
1653
  }}
1614
1654
 
1615
1655
  panel.style.display = 'block';
1616
- tbody.innerHTML = filtered.map(f => `<tr>
1656
+ tbody.innerHTML = filtered.map(f => {{
1657
+ const kindTag = f.task_kind === 'chore' || f.chore ? '[C]' : '[P]';
1658
+ const target = f.task_target || f.process || f.chore || '';
1659
+ return `<tr>
1617
1660
  <td class="mono">${{f.task_id}}</td>
1618
- <td>${{f.process}}</td>
1661
+ <td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
1619
1662
  <td>${{f.instance}}</td>
1620
1663
  <td class="mono">${{f.run_id}}</td>
1621
1664
  <td class="mono">${{f.duration_seconds != null ? f.duration_seconds.toFixed(2) : '-'}}</td>
1622
1665
  <td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
1623
1666
  title="${{(f.error_message || '').replace(/"/g, '&quot;')}}">${{f.error_message || '-'}}</td>
1624
- </tr>`).join('');
1667
+ </tr>`;
1668
+ }}).join('');
1625
1669
  }}
1626
1670
 
1627
1671
  function updateConfigDetails(runs) {{