detectkit 0.49.1__tar.gz → 0.49.2__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 (152) hide show
  1. {detectkit-0.49.1/detectkit.egg-info → detectkit-0.49.2}/PKG-INFO +1 -1
  2. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/__init__.py +1 -1
  3. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_decision.py +2 -1
  4. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_replay.py +24 -7
  5. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/assets/ui.js +10 -4
  6. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/server.py +6 -1
  7. {detectkit-0.49.1 → detectkit-0.49.2/detectkit.egg-info}/PKG-INFO +1 -1
  8. {detectkit-0.49.1 → detectkit-0.49.2}/LICENSE +0 -0
  9. {detectkit-0.49.1 → detectkit-0.49.2}/MANIFEST.in +0 -0
  10. {detectkit-0.49.1 → detectkit-0.49.2}/README.md +0 -0
  11. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/__init__.py +0 -0
  12. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/__init__.py +0 -0
  13. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/base.py +0 -0
  14. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/branding.py +0 -0
  15. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/email.py +0 -0
  16. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/factory.py +0 -0
  17. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/mattermost.py +0 -0
  18. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/slack.py +0 -0
  19. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/telegram.py +0 -0
  20. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/webhook.py +0 -0
  21. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/__init__.py +0 -0
  22. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_base.py +0 -0
  23. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  24. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  25. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  26. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_types.py +0 -0
  27. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  28. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/__init__.py +0 -0
  29. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/_base.py +0 -0
  30. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/_types.py +0 -0
  31. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/autotuner.py +0 -0
  32. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/config_emitter.py +0 -0
  33. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/crossval.py +0 -0
  34. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/detector_select.py +0 -0
  35. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/distribution.py +0 -0
  36. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/grid_search.py +0 -0
  37. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/labels.py +0 -0
  38. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/result.py +0 -0
  39. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/runner.py +0 -0
  40. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/scoring.py +0 -0
  41. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/seasonality_search.py +0 -0
  42. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/settings.py +0 -0
  43. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/window_select.py +0 -0
  44. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/__init__.py +0 -0
  45. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/_output.py +0 -0
  46. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  47. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  48. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  49. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  50. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  51. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  52. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  53. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/project.md +0 -0
  54. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  55. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  56. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  57. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  58. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  59. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/__init__.py +0 -0
  60. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/autotune.py +0 -0
  61. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/clean.py +0 -0
  62. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/init.py +0 -0
  63. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/init_claude.py +0 -0
  64. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/osi.py +0 -0
  65. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/run.py +0 -0
  66. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/test_alert.py +0 -0
  67. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/tune.py +0 -0
  68. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/ui.py +0 -0
  69. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/unlock.py +0 -0
  70. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/main.py +0 -0
  71. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/__init__.py +0 -0
  72. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/metric_config.py +0 -0
  73. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/profile.py +0 -0
  74. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/project_config.py +0 -0
  75. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/validator.py +0 -0
  76. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/__init__.py +0 -0
  77. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/interval.py +0 -0
  78. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/models.py +0 -0
  79. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/__init__.py +0 -0
  80. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/_sql_manager.py +0 -0
  81. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/clickhouse_manager.py +0 -0
  82. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/__init__.py +0 -0
  83. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_alert_states.py +0 -0
  84. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  85. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_base.py +0 -0
  86. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_datapoints.py +0 -0
  87. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_detections.py +0 -0
  88. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_maintenance.py +0 -0
  89. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_metrics.py +0 -0
  90. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_schema.py +0 -0
  91. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_tasks.py +0 -0
  92. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/manager.py +0 -0
  93. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/manager.py +0 -0
  94. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/mysql_manager.py +0 -0
  95. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/postgres_manager.py +0 -0
  96. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/tables.py +0 -0
  97. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/__init__.py +0 -0
  98. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/base.py +0 -0
  99. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/factory.py +0 -0
  100. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/seasonality.py +0 -0
  101. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/__init__.py +0 -0
  102. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/_windowed.py +0 -0
  103. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/iqr.py +0 -0
  104. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/mad.py +0 -0
  105. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  106. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/zscore.py +0 -0
  107. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/__init__.py +0 -0
  108. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/metric_loader.py +0 -0
  109. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/query_template.py +0 -0
  110. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/__init__.py +0 -0
  111. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/error_dispatch.py +0 -0
  112. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/__init__.py +0 -0
  113. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  114. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_base.py +0 -0
  115. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  116. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  117. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_types.py +0 -0
  118. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/manager.py +0 -0
  119. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/__init__.py +0 -0
  120. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/assets/report.js +0 -0
  121. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/builder.py +0 -0
  122. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/html_report.py +0 -0
  123. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/__init__.py +0 -0
  124. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/errors.py +0 -0
  125. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/exporter.py +0 -0
  126. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/importer.py +0 -0
  127. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/osi_model.py +0 -0
  128. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/query_gen.py +0 -0
  129. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/__init__.py +0 -0
  130. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/assets/tune.js +0 -0
  131. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/config_writer.py +0 -0
  132. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/html.py +0 -0
  133. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/payload.py +0 -0
  134. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/server.py +0 -0
  135. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/__init__.py +0 -0
  136. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/html.py +0 -0
  137. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/jobs.py +0 -0
  138. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/overview.py +0 -0
  139. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/__init__.py +0 -0
  140. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/datetime_utils.py +0 -0
  141. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/env_interpolation.py +0 -0
  142. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/json_utils.py +0 -0
  143. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/stats.py +0 -0
  144. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/SOURCES.txt +0 -0
  145. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/dependency_links.txt +0 -0
  146. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/entry_points.txt +0 -0
  147. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/requires.txt +0 -0
  148. {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/top_level.txt +0 -0
  149. {detectkit-0.49.1 → detectkit-0.49.2}/pyproject.toml +0 -0
  150. {detectkit-0.49.1 → detectkit-0.49.2}/requirements.txt +0 -0
  151. {detectkit-0.49.1 → detectkit-0.49.2}/setup.cfg +0 -0
  152. {detectkit-0.49.1 → detectkit-0.49.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.49.1
3
+ Version: 0.49.2
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.49.1"
7
+ __version__ = "0.49.2"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -29,6 +29,7 @@ The multi-detector alert contract (documented in docs/guides/alerting.md):
29
29
  from __future__ import annotations
30
30
 
31
31
  import math
32
+ from collections.abc import Sequence
32
33
  from datetime import datetime, timezone
33
34
 
34
35
  import numpy as np
@@ -166,7 +167,7 @@ class _DecisionMixin(_OrchestratorBase):
166
167
  def _count_consecutive_anomalies(
167
168
  self,
168
169
  detections_by_time: dict[np.datetime64, list[DetectionRecord]],
169
- timestamps_sorted: list[np.datetime64],
170
+ timestamps_sorted: Sequence[np.datetime64],
170
171
  ) -> tuple[int, list[DetectionRecord] | None, str | None]:
171
172
  """Walk timestamps newest→oldest counting quorum-satisfying points.
172
173
 
@@ -16,6 +16,8 @@ mutating ``_dtk_alert_states`` would be wrong.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ from collections import deque
20
+ from collections.abc import Sequence
19
21
  from dataclasses import dataclass
20
22
  from datetime import datetime, timedelta
21
23
 
@@ -76,7 +78,24 @@ class _ReplayMixin(_OrchestratorBase):
76
78
  sim_last_recovery: np.datetime64 | None = None
77
79
  events: list[ReplayedEvent] = []
78
80
 
81
+ # The causal view grows monotonically as the grid advances, so it is
82
+ # maintained incrementally: one sorted pass up front, then each grid
83
+ # step admits the newly-covered timestamps (appendleft keeps ts_desc
84
+ # newest-first at O(1)). Rebuilding the dict + re-sorting per grid
85
+ # point — the previous shape — is O(n^2 log n) and turns a month of a
86
+ # 1-minute metric into a multi-minute replay.
87
+ all_ts_asc = sorted(by_time)
88
+ causal: dict[np.datetime64, list[DetectionRecord]] = {}
89
+ ts_desc: deque[np.datetime64] = deque()
90
+ next_ts = 0
91
+
79
92
  for t in self._replay_grid(start, end):
93
+ while next_ts < len(all_ts_asc) and all_ts_asc[next_ts] <= t:
94
+ ts = all_ts_asc[next_ts]
95
+ causal[ts] = by_time[ts]
96
+ ts_desc.appendleft(ts)
97
+ next_ts += 1
98
+
80
99
  # No-data fires independently of the quorum (a single binary
81
100
  # metric-level signal), only when configured and not in cooldown.
82
101
  if (
@@ -92,9 +111,6 @@ class _ReplayMixin(_OrchestratorBase):
92
111
  sim_last_alert = t
93
112
  continue
94
113
 
95
- causal = {ts: recs for ts, recs in by_time.items() if ts <= t}
96
- ts_desc = sorted(causal, reverse=True)
97
-
98
114
  consecutive, latest_quorum, direction = self._count_consecutive_anomalies(
99
115
  causal, ts_desc
100
116
  )
@@ -170,7 +186,7 @@ class _ReplayMixin(_OrchestratorBase):
170
186
  def _replay_recovered(
171
187
  self,
172
188
  causal: dict[np.datetime64, list[DetectionRecord]],
173
- ts_desc: list[np.datetime64],
189
+ ts_desc: Sequence[np.datetime64],
174
190
  sim_last_alert: np.datetime64,
175
191
  ) -> bool:
176
192
  """Pure half of :meth:`_RecoveryMixin._check_recovery_since_last_alert`.
@@ -184,8 +200,9 @@ class _ReplayMixin(_OrchestratorBase):
184
200
  return True
185
201
 
186
202
  # No fresh detections after the alert → assume recovery (mirrors the
187
- # live "no fresh detections" branch).
188
- if not any(ts > sim_last_alert for ts in ts_desc):
203
+ # live "no fresh detections" branch). ts_desc is newest-first, so the
204
+ # head carries the maximum no full scan needed.
205
+ if not ts_desc[0] > sim_last_alert:
189
206
  return True
190
207
 
191
208
  latest_ts = ts_desc[0]
@@ -242,7 +259,7 @@ class _ReplayMixin(_OrchestratorBase):
242
259
  def _replay_streak(
243
260
  self,
244
261
  causal: dict[np.datetime64, list[DetectionRecord]],
245
- ts_desc: list[np.datetime64],
262
+ ts_desc: Sequence[np.datetime64],
246
263
  ) -> tuple[int, np.datetime64, bool]:
247
264
  """In-memory analog of :meth:`_DecisionMixin._resolve_streak`.
248
265
 
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var k=e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ke(e){let n=e/60;return e>=86400&&e%86400===0?e/86400+"d":e>=3600&&e%3600===0?e/3600+"h":n>=1&&e%60===0?n+"min":e+"s"}function F(e){return e==null||!Number.isFinite(e)?"\u2014":(e*100).toFixed(1)+"%"}function oe(e){return e==null||!Number.isFinite(e)?"\u2014":Math.round(e).toLocaleString("en-US")}function V(e){return e==null||!Number.isFinite(e)?"\u2014":`\u2248${e>=9.5?e.toFixed(0):e.toFixed(1)}/day`}function le(e,n){let r=Math.max(0,e-n),t=Math.round(r/6e4);if(t<1)return"just now";if(t<60)return`${t}m ago`;let o=Math.floor(t/60);if(o<24)return`${o}h ago`;let a=Math.floor(o/24);return a<30?`${a}d ago`:`${Math.floor(a/30)}mo ago`}function he(e,n){let r=Math.max(0,Math.round((n-e)/1e3));if(r<60)return`${r}s`;let t=Math.floor(r/60),o=r%60;if(t<60)return o?`${t}m ${o}s`:`${t}m`;let a=Math.floor(t/60),c=t%60;return`${a}h ${String(c).padStart(2,"0")}m`}function me(e){return new Date(e).toISOString().slice(0,19).replace("T"," ")}function de(e){let n=Math.round(e/60);if(n<60)return`${n}m`;let r=Math.floor(n/60),t=n%60;if(r<24)return r+"h"+(t?` ${t}m`:"");let o=Math.floor(r/24),a=r%24;return o+"d"+(a?` ${a}h`:"")}function xe(e,n){let r=new Map;for(let t of e){let o=n(t),a=r.get(o);a?a.push(t):r.set(o,[t])}return r}var Ye=new URLSearchParams(location.search).get("token")||"";function fe(e,n){let r=new URL(e,location.origin);if(r.searchParams.set("token",Ye),n)for(let[t,o]of Object.entries(n))r.searchParams.set(t,o);return r.toString()}function be(e,n){return fe(`/metric/${encodeURIComponent(e)}`,{window:n})}async function ye(e){let n=await e.text().catch(()=>"");return new Error(n||`HTTP ${e.status}`)}async function ge(e,n){let r=await fetch(fe(e,n));if(!r.ok)throw await ye(r);return r.json()}async function re(e,n){let r=await fetch(fe(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok)throw await ye(r);return r.json()}function we(e,n){return ge(`/api/stats/${encodeURIComponent(e)}`,{window:n})}function Te(){return ge("/api/jobs")}function Ce(e,n){return ge(`/api/job/${encodeURIComponent(e)}`,{offset:String(n)})}function Me(e){return re("/api/run",e)}function Se(e){return re("/api/autotune",e)}function Ee(e){return re("/api/unlock",e)}function $e(e){return re("/api/tune",e)}function Le(e){return re(`/api/job/${encodeURIComponent(e)}/stop`,{})}var Y="dtk-ui",Re=!1;function Ne(){if(Re)return;Re=!0;let e=`
1
+ "use strict";(()=>{var k=e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ke(e){let n=e/60;return e>=86400&&e%86400===0?e/86400+"d":e>=3600&&e%3600===0?e/3600+"h":n>=1&&e%60===0?n+"min":e+"s"}function F(e){return e==null||!Number.isFinite(e)?"\u2014":(e*100).toFixed(1)+"%"}function oe(e){return e==null||!Number.isFinite(e)?"\u2014":Math.round(e).toLocaleString("en-US")}function V(e){return e==null||!Number.isFinite(e)?"\u2014":`\u2248${e>=9.5?e.toFixed(0):e.toFixed(1)}/day`}function le(e,n){let r=Math.max(0,e-n),t=Math.round(r/6e4);if(t<1)return"just now";if(t<60)return`${t}m ago`;let o=Math.floor(t/60);if(o<24)return`${o}h ago`;let a=Math.floor(o/24);return a<30?`${a}d ago`:`${Math.floor(a/30)}mo ago`}function he(e,n){let r=Math.max(0,Math.round((n-e)/1e3));if(r<60)return`${r}s`;let t=Math.floor(r/60),o=r%60;if(t<60)return o?`${t}m ${o}s`:`${t}m`;let a=Math.floor(t/60),c=t%60;return`${a}h ${String(c).padStart(2,"0")}m`}function me(e){return new Date(e).toISOString().slice(0,19).replace("T"," ")}function de(e){let n=Math.round(e/60);if(n<60)return`${n}m`;let r=Math.floor(n/60),t=n%60;if(r<24)return r+"h"+(t?` ${t}m`:"");let o=Math.floor(r/24),a=r%24;return o+"d"+(a?` ${a}h`:"")}function xe(e,n){let r=new Map;for(let t of e){let o=n(t),a=r.get(o);a?a.push(t):r.set(o,[t])}return r}var Ye=new URLSearchParams(location.search).get("token")||"";function fe(e,n){let r=new URL(e,location.origin);if(r.searchParams.set("token",Ye),n)for(let[t,o]of Object.entries(n))r.searchParams.set(t,o);return r.toString()}function be(e,n){return fe(`/metric/${encodeURIComponent(e)}`,{window:n})}async function ye(e){let n=await e.text().catch(()=>"");return new Error(n||`HTTP ${e.status}`)}async function ge(e,n){let r=await fetch(fe(e,n));if(!r.ok)throw await ye(r);return r.json()}async function re(e,n){let r=await fetch(fe(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok)throw await ye(r);return r.json()}function we(e,n){return ge(`/api/stats/${encodeURIComponent(e)}`,{window:n})}function Te(){return ge("/api/jobs")}function Ce(e,n){return ge(`/api/job/${encodeURIComponent(e)}`,{offset:String(n)})}function Me(e){return re("/api/run",e)}function Se(e){return re("/api/autotune",e)}function Ee(e){return re("/api/unlock",e)}function Le(e){return re("/api/tune",e)}function $e(e){return re(`/api/job/${encodeURIComponent(e)}/stop`,{})}var Y="dtk-ui",Re=!1;function Ne(){if(Re)return;Re=!0;let e=`
2
2
  .${Y}{
3
3
  --clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;
4
4
  --muted:#6e675b;--faint:#9a9384;
@@ -160,8 +160,14 @@
160
160
  .dtk-ui-overlay-close{border:1px solid var(--border);background:transparent;color:var(--muted);
161
161
  border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:14px;}
162
162
  .dtk-ui-overlay-close:hover{border-color:var(--st-anomaly);color:var(--st-anomaly);}
163
- .dtk-ui-overlay-body{flex:1;min-height:0;}
163
+ .dtk-ui-overlay-body{flex:1;min-height:0;position:relative;background:var(--term-bg);}
164
164
  .dtk-ui-overlay-body iframe{width:100%;height:100%;border:0;display:block;background:#fff;}
165
+ .dtk-ui-overlay-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
166
+ gap:10px;color:var(--term-text);font-family:var(--mono);font-size:13px;}
167
+ .dtk-ui-overlay-loading b{color:var(--clay);font-weight:600;}
168
+ .dtk-ui-overlay-spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--term-border);
169
+ border-top-color:var(--clay);animation:dtk-ui-spin 0.8s linear infinite;flex:none;}
170
+ @keyframes dtk-ui-spin{to{transform:rotate(360deg);}}
165
171
 
166
172
  /* --- drawers (run panel + jobs) --------------------------------------------- */
167
173
  .dtk-ui-drawer-backdrop{position:fixed;inset:0;background:rgba(10,9,7,0.5);z-index:40;display:none;}
@@ -244,6 +250,6 @@
244
250
  .dtk-ui-row2{grid-template-columns:1fr;}
245
251
  .dtk-ui-table-wrap{overflow-x:auto;}
246
252
  }
247
- `,n=document.createElement("style");n.setAttribute("data-dtk-ui",""),n.textContent=e,document.head.appendChild(n)}var K=null;function Ke(e){return K&&K.isConnected||(K=document.createElement("div"),K.className="dtk-toasts",e.appendChild(K)),K}function B(e,n,r){let t=Ke(e),o=document.createElement("div");o.className=`dtk-toast dtk-toast-${n}`,o.textContent=r,t.appendChild(o),window.setTimeout(()=>{o.classList.add("dtk-toast-out"),window.setTimeout(()=>o.remove(),220)},5e3)}function W(e,n,r,t){let o="dtk-ui-tile"+(t!=null&&t.err?" err":""),a=t!=null&&t.warn?"dtk-ui-tile-sub warn":"dtk-ui-tile-sub";return`<div class="${o}"><div class="dtk-ui-tile-val">${k(e)}</div><div class="dtk-ui-tile-label">${k(n)}</div>`+(r?`<div class="${a}">${k(r)}</div>`:"")+"</div>"}function Pe(e){let n=document.createElement("div");n.className="dtk-ui-tiles";let r=e.length,t=e.filter(s=>s.enabled).length,o=e.filter(s=>!s.pending),a=0,c=0,v=0,w=0,p=!1;for(let s of o)a+=s.alerts.anomaly,c+=s.alerts.no_data,s.alerts.anomaly>0&&v++,s.alerts.per_day!==null&&(w+=s.alerts.per_day,p=!0);let m=[];for(let s of o)if(s.enabled){if(s.last_point===null){m.push({m:s,lag:1/0});continue}s.lag_seconds!==null&&s.interval_seconds>0&&s.lag_seconds>2*s.interval_seconds&&m.push({m:s,lag:s.lag_seconds})}m.sort((s,b)=>b.lag-s.lag);let h=m.length===0?void 0:`worst: ${m[0].m.name}${Number.isFinite(m[0].lag)?` (${de(m[0].lag)})`:" (no data)"}`;n.innerHTML=W(`${t}/${r}`,"Metrics","enabled / total")+W(oe(a),"Alerts in window",p?V(w):void 0)+W(oe(c),"No-data events")+W(oe(v),"Metrics alerting")+W(oe(m.length),"Stale metrics",h,{warn:m.length>0,err:m.length>0});let g=o.filter(s=>s.quality!==null);if(g.length>0){let s=0,b=0,x=0,M=0,u=0;for(let S of g){let E=S.quality;E&&(s+=E.caught,b+=E.incidents_in_window,x+=E.false_alerts,M+=S.alerts.anomaly,S.budget>u&&(u=S.budget))}let $=b>0?s/b:null,l=M>0?x/M:null,f=l!==null&&u>0&&l>u;n.innerHTML+=W(F($),"Labeled recall",`${g.length} metric(s) labeled`)+W(F(l),"False-alert rate",f?`\u25B2 over ${F(u)} budget`:void 0,{warn:f})}return n}var ae="untagged";function We(e){let n=new Map,r=(t,o)=>{let a=n.get(t);a||(a={tag:t,count:0,alerts:0,perDaySum:0,havePerDay:!1},n.set(t,a)),a.count++,a.alerts+=o.alerts.anomaly,o.alerts.per_day!==null&&(a.perDaySum+=o.alerts.per_day,a.havePerDay=!0)};for(let t of e)if(t.tags.length===0)r(ae,t);else for(let o of t.tags)r(o,t);return[...n.values()].sort((t,o)=>o.count-t.count||t.tag.localeCompare(o.tag))}function De(e,n,r){let t=document.createElement("div");t.className="dtk-ui-tags";let o=document.createElement("button");o.type="button",o.className="dtk-ui-tag"+(n===null?" on":""),o.innerHTML=`<span class="dtk-ui-tag-name">All</span><span class="dtk-ui-tag-n">${e.length}</span>`,o.onclick=()=>r(null),t.appendChild(o);for(let a of We(e)){let c=document.createElement("button");c.type="button",c.className="dtk-ui-tag"+(n===a.tag?" on":"");let v=a.havePerDay?` \xB7 ${V(a.perDaySum)}`:"";c.innerHTML=`<span class="dtk-ui-tag-name">${k(a.tag===ae?ae:`#${a.tag}`)}</span><span class="dtk-ui-tag-n">${a.count} metric${a.count===1?"":"s"}</span><span class="dtk-ui-tag-n">${a.alerts} alert${a.alerts===1?"":"s"}</span>`+(v?`<span class="dtk-ui-tag-sub">${k(v)}</span>`:""),c.onclick=()=>r(a.tag),t.appendChild(c)}return t}var Ge={"--term-bg":"#211e1a","--clay":"#d15b36","--st-anomaly":"#d63232","--st-recovery":"#36a64f","--st-nodata":"#f0ad4e","--st-error":"#5a7a8c","--faint":"#9a9384","--muted":"#6e675b","--border":"#332f29","--term-border":"#332f29"};function ce(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||Ge[e]||"#888"}function Xe(e){let n=e.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let r=parseInt(n,16);return n.length!==6||Number.isNaN(r)?[209,91,54]:[r>>16&255,r>>8&255,r&255]}function _e(e,n){let[r,t,o]=Xe(e);return`rgba(${r},${t},${o},${n})`}var vt=Number.isFinite;var He=140,je=30;function Oe(e,n,r){let t=Math.max(1,window.devicePixelRatio||1);e.style.width=`${He}px`,e.style.height=`${je}px`,e.width=Math.round(He*t),e.height=Math.round(je*t);let o=e.getContext("2d");if(!o||(o.clearRect(0,0,e.width,e.height),n.length===0))return;let a=3*t,c=e.width,v=e.height,w=n[0].t,p=n[n.length-1].t,m=p-w||1,h=l=>a+(l-w)/m*Math.max(1,c-2*a),g=1/0,s=-1/0;for(let l of n)l.v!==null&&Number.isFinite(l.v)&&(l.v<g&&(g=l.v),l.v>s&&(s=l.v));let b=!Number.isFinite(g)||!Number.isFinite(s);b&&(g=0,s=1),s<=g&&(s=g+1);let x=l=>v-a-(l-g)/(s-g)*Math.max(1,v-2*a);if(b){let l=v/2;o.strokeStyle=_e(ce("--faint"),.5),o.lineWidth=1*t,o.setLineDash([2*t,2*t]),o.beginPath(),o.moveTo(a,l),o.lineTo(c-a,l),o.stroke(),o.setLineDash([]);return}o.strokeStyle=ce("--term-text"),o.lineWidth=1*t,o.lineJoin="round",o.beginPath();let M=!1;for(let l of n){if(l.v===null||!Number.isFinite(l.v)){M=!1;continue}let f=h(l.t),S=x(l.v);M?o.lineTo(f,S):(o.moveTo(f,S),M=!0)}if(o.stroke(),r.length===0)return;let u=[];for(let l of n)l.v!==null&&Number.isFinite(l.v)&&u.push([l.t,l.v]);let $=l=>{if(u.length===0)return null;if(l<=u[0][0])return u[0][1];if(l>=u[u.length-1][0])return u[u.length-1][1];for(let f=1;f<u.length;f++){let[S,E]=u[f];if(l<=S){let[R,P]=u[f-1],O=S===R?0:(l-R)/(S-R);return P+(E-P)*O}}return u[u.length-1][1]};o.fillStyle=ce("--st-anomaly");for(let l of r){if(l<w||l>p)continue;let f=$(l);f!==null&&(o.beginPath(),o.arc(h(l),x(f),2*t,0,Math.PI*2),o.fill())}}var ue='<span class="dtk-ui-pending">\xB7\xB7\xB7</span>';function pe(e){return{name:e.name,dir:e.dir,file:e.file,tags:e.tags,enabled:e.enabled,interval_seconds:e.interval_seconds,detectors:[],alert_rule:null,last_point:null,first_point_in_window:null,lag_seconds:null,locked:!1,points:0,flagged:0,anomaly_rate:null,alerts:{anomaly:0,recovery:0,no_data:0,per_day:null,last_ts:null},quality:null,budget:0,spark:[],spark_anoms:[],error:null,pending:!0}}var ve={alerts:"desc",name:"asc",rate:"desc",freshness:"desc"};function Je(e){var o;if(e.pending)return{color:"var(--faint)",title:"loading\u2026",rank:0};if(!e.enabled)return{color:"var(--faint)",title:"disabled",rank:-1};if(e.last_point===null)return{color:"var(--st-anomaly)",title:"no datapoints loaded yet",rank:1/0};let n=(o=e.lag_seconds)!=null?o:0,r=e.interval_seconds>0?n/e.interval_seconds:0,t=`lag ${de(Math.max(0,n))} (${r.toFixed(1)}\xD7 interval) \xB7 last point ${me(e.last_point)} UTC`;return r<2?{color:"var(--st-recovery)",title:t,rank:n}:r<6?{color:"var(--st-nodata)",title:t,rank:n}:{color:"var(--st-anomaly)",title:t,rank:n}}function Qe(e){return e.length===0?"":`<div class="dtk-ui-tagchips">${e.map(n=>`<span class="dtk-ui-tagchip">${k(n)}</span>`).join("")}</div>`}function Ve(e){let n=e.quality;if(!n)return'<span class="dtk-ui-quality empty">\u2014</span>';let r=`Incidents: ${n.incidents} (${n.incidents_in_window} in window) \xB7 caught ${n.caught} \xB7 false alerts ${n.false_alerts} \xB7 reviewed ${n.reviewed} (valid ${n.reviewed_valid}, false ${n.reviewed_false}) \xB7 ${n.labels_file}`;return`<span class="dtk-ui-quality" title="${k(r)}"><span class="dtk-ui-quality-chip">R <b>${k(F(n.recall))}</b></span> \xB7 <span class="dtk-ui-quality-chip">FDR <b>${k(F(n.fdr))}</b></span> \xB7 <span class="dtk-ui-quality-chip">\u2713${n.reviewed_valid}</span></span>`}function Ze(e){let n=e.alert_rule?`min_detectors=${e.alert_rule.min_detectors} \xB7 direction=${e.alert_rule.direction} \xB7 consecutive=${e.alert_rule.consecutive} (${e.alert_rule.enabled}/${e.alert_rule.configs} config(s) enabled)`:"no alerting configured";return`detectors: ${e.detectors.join(", ")||"\u2014"}
253
+ `,n=document.createElement("style");n.setAttribute("data-dtk-ui",""),n.textContent=e,document.head.appendChild(n)}var K=null;function Ke(e){return K&&K.isConnected||(K=document.createElement("div"),K.className="dtk-toasts",e.appendChild(K)),K}function B(e,n,r){let t=Ke(e),o=document.createElement("div");o.className=`dtk-toast dtk-toast-${n}`,o.textContent=r,t.appendChild(o),window.setTimeout(()=>{o.classList.add("dtk-toast-out"),window.setTimeout(()=>o.remove(),220)},5e3)}function W(e,n,r,t){let o="dtk-ui-tile"+(t!=null&&t.err?" err":""),a=t!=null&&t.warn?"dtk-ui-tile-sub warn":"dtk-ui-tile-sub";return`<div class="${o}"><div class="dtk-ui-tile-val">${k(e)}</div><div class="dtk-ui-tile-label">${k(n)}</div>`+(r?`<div class="${a}">${k(r)}</div>`:"")+"</div>"}function Pe(e){let n=document.createElement("div");n.className="dtk-ui-tiles";let r=e.length,t=e.filter(s=>s.enabled).length,o=e.filter(s=>!s.pending),a=0,c=0,h=0,w=0,p=!1;for(let s of o)a+=s.alerts.anomaly,c+=s.alerts.no_data,s.alerts.anomaly>0&&h++,s.alerts.per_day!==null&&(w+=s.alerts.per_day,p=!0);let m=[];for(let s of o)if(s.enabled){if(s.last_point===null){m.push({m:s,lag:1/0});continue}s.lag_seconds!==null&&s.interval_seconds>0&&s.lag_seconds>2*s.interval_seconds&&m.push({m:s,lag:s.lag_seconds})}m.sort((s,g)=>g.lag-s.lag);let x=m.length===0?void 0:`worst: ${m[0].m.name}${Number.isFinite(m[0].lag)?` (${de(m[0].lag)})`:" (no data)"}`;n.innerHTML=W(`${t}/${r}`,"Metrics","enabled / total")+W(oe(a),"Alerts in window",p?V(w):void 0)+W(oe(c),"No-data events")+W(oe(h),"Metrics alerting")+W(oe(m.length),"Stale metrics",x,{warn:m.length>0,err:m.length>0});let b=o.filter(s=>s.quality!==null);if(b.length>0){let s=0,g=0,v=0,T=0,u=0;for(let S of b){let E=S.quality;E&&(s+=E.caught,g+=E.incidents_in_window,v+=E.false_alerts,T+=S.alerts.anomaly,S.budget>u&&(u=S.budget))}let L=g>0?s/g:null,l=T>0?v/T:null,f=l!==null&&u>0&&l>u;n.innerHTML+=W(F(L),"Labeled recall",`${b.length} metric(s) labeled`)+W(F(l),"False-alert rate",f?`\u25B2 over ${F(u)} budget`:void 0,{warn:f})}return n}var ae="untagged";function We(e){let n=new Map,r=(t,o)=>{let a=n.get(t);a||(a={tag:t,count:0,alerts:0,perDaySum:0,havePerDay:!1},n.set(t,a)),a.count++,a.alerts+=o.alerts.anomaly,o.alerts.per_day!==null&&(a.perDaySum+=o.alerts.per_day,a.havePerDay=!0)};for(let t of e)if(t.tags.length===0)r(ae,t);else for(let o of t.tags)r(o,t);return[...n.values()].sort((t,o)=>o.count-t.count||t.tag.localeCompare(o.tag))}function De(e,n,r){let t=document.createElement("div");t.className="dtk-ui-tags";let o=document.createElement("button");o.type="button",o.className="dtk-ui-tag"+(n===null?" on":""),o.innerHTML=`<span class="dtk-ui-tag-name">All</span><span class="dtk-ui-tag-n">${e.length}</span>`,o.onclick=()=>r(null),t.appendChild(o);for(let a of We(e)){let c=document.createElement("button");c.type="button",c.className="dtk-ui-tag"+(n===a.tag?" on":"");let h=a.havePerDay?` \xB7 ${V(a.perDaySum)}`:"";c.innerHTML=`<span class="dtk-ui-tag-name">${k(a.tag===ae?ae:`#${a.tag}`)}</span><span class="dtk-ui-tag-n">${a.count} metric${a.count===1?"":"s"}</span><span class="dtk-ui-tag-n">${a.alerts} alert${a.alerts===1?"":"s"}</span>`+(h?`<span class="dtk-ui-tag-sub">${k(h)}</span>`:""),c.onclick=()=>r(a.tag),t.appendChild(c)}return t}var Ge={"--term-bg":"#211e1a","--clay":"#d15b36","--st-anomaly":"#d63232","--st-recovery":"#36a64f","--st-nodata":"#f0ad4e","--st-error":"#5a7a8c","--faint":"#9a9384","--muted":"#6e675b","--border":"#332f29","--term-border":"#332f29"};function ce(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||Ge[e]||"#888"}function Xe(e){let n=e.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let r=parseInt(n,16);return n.length!==6||Number.isNaN(r)?[209,91,54]:[r>>16&255,r>>8&255,r&255]}function _e(e,n){let[r,t,o]=Xe(e);return`rgba(${r},${t},${o},${n})`}var vt=Number.isFinite;var He=140,je=30;function Oe(e,n,r){let t=Math.max(1,window.devicePixelRatio||1);e.style.width=`${He}px`,e.style.height=`${je}px`,e.width=Math.round(He*t),e.height=Math.round(je*t);let o=e.getContext("2d");if(!o||(o.clearRect(0,0,e.width,e.height),n.length===0))return;let a=3*t,c=e.width,h=e.height,w=n[0].t,p=n[n.length-1].t,m=p-w||1,x=l=>a+(l-w)/m*Math.max(1,c-2*a),b=1/0,s=-1/0;for(let l of n)l.v!==null&&Number.isFinite(l.v)&&(l.v<b&&(b=l.v),l.v>s&&(s=l.v));let g=!Number.isFinite(b)||!Number.isFinite(s);g&&(b=0,s=1),s<=b&&(s=b+1);let v=l=>h-a-(l-b)/(s-b)*Math.max(1,h-2*a);if(g){let l=h/2;o.strokeStyle=_e(ce("--faint"),.5),o.lineWidth=1*t,o.setLineDash([2*t,2*t]),o.beginPath(),o.moveTo(a,l),o.lineTo(c-a,l),o.stroke(),o.setLineDash([]);return}o.strokeStyle=ce("--term-text"),o.lineWidth=1*t,o.lineJoin="round",o.beginPath();let T=!1;for(let l of n){if(l.v===null||!Number.isFinite(l.v)){T=!1;continue}let f=x(l.t),S=v(l.v);T?o.lineTo(f,S):(o.moveTo(f,S),T=!0)}if(o.stroke(),r.length===0)return;let u=[];for(let l of n)l.v!==null&&Number.isFinite(l.v)&&u.push([l.t,l.v]);let L=l=>{if(u.length===0)return null;if(l<=u[0][0])return u[0][1];if(l>=u[u.length-1][0])return u[u.length-1][1];for(let f=1;f<u.length;f++){let[S,E]=u[f];if(l<=S){let[R,P]=u[f-1],O=S===R?0:(l-R)/(S-R);return P+(E-P)*O}}return u[u.length-1][1]};o.fillStyle=ce("--st-anomaly");for(let l of r){if(l<w||l>p)continue;let f=L(l);f!==null&&(o.beginPath(),o.arc(x(l),v(f),2*t,0,Math.PI*2),o.fill())}}var ue='<span class="dtk-ui-pending">\xB7\xB7\xB7</span>';function pe(e){return{name:e.name,dir:e.dir,file:e.file,tags:e.tags,enabled:e.enabled,interval_seconds:e.interval_seconds,detectors:[],alert_rule:null,last_point:null,first_point_in_window:null,lag_seconds:null,locked:!1,points:0,flagged:0,anomaly_rate:null,alerts:{anomaly:0,recovery:0,no_data:0,per_day:null,last_ts:null},quality:null,budget:0,spark:[],spark_anoms:[],error:null,pending:!0}}var ve={alerts:"desc",name:"asc",rate:"desc",freshness:"desc"};function Je(e){var o;if(e.pending)return{color:"var(--faint)",title:"loading\u2026",rank:0};if(!e.enabled)return{color:"var(--faint)",title:"disabled",rank:-1};if(e.last_point===null)return{color:"var(--st-anomaly)",title:"no datapoints loaded yet",rank:1/0};let n=(o=e.lag_seconds)!=null?o:0,r=e.interval_seconds>0?n/e.interval_seconds:0,t=`lag ${de(Math.max(0,n))} (${r.toFixed(1)}\xD7 interval) \xB7 last point ${me(e.last_point)} UTC`;return r<2?{color:"var(--st-recovery)",title:t,rank:n}:r<6?{color:"var(--st-nodata)",title:t,rank:n}:{color:"var(--st-anomaly)",title:t,rank:n}}function Qe(e){return e.length===0?"":`<div class="dtk-ui-tagchips">${e.map(n=>`<span class="dtk-ui-tagchip">${k(n)}</span>`).join("")}</div>`}function Ve(e){let n=e.quality;if(!n)return'<span class="dtk-ui-quality empty">\u2014</span>';let r=`Incidents: ${n.incidents} (${n.incidents_in_window} in window) \xB7 caught ${n.caught} \xB7 false alerts ${n.false_alerts} \xB7 reviewed ${n.reviewed} (valid ${n.reviewed_valid}, false ${n.reviewed_false}) \xB7 ${n.labels_file}`;return`<span class="dtk-ui-quality" title="${k(r)}"><span class="dtk-ui-quality-chip">R <b>${k(F(n.recall))}</b></span> \xB7 <span class="dtk-ui-quality-chip">FDR <b>${k(F(n.fdr))}</b></span> \xB7 <span class="dtk-ui-quality-chip">\u2713${n.reviewed_valid}</span></span>`}function Ze(e){let n=e.alert_rule?`min_detectors=${e.alert_rule.min_detectors} \xB7 direction=${e.alert_rule.direction} \xB7 consecutive=${e.alert_rule.consecutive} (${e.alert_rule.enabled}/${e.alert_rule.configs} config(s) enabled)`:"no alerting configured";return`detectors: ${e.detectors.join(", ")||"\u2014"}
248
254
  alert rule: ${n}
249
- file: ${e.file}`}function et(e,n,r,t){let o=document.createElement("tr");o.className="dtk-ui-row"+(e.enabled?"":" disabled")+(e.error?" errored":"")+(e.pending?" pending":"");let a=Je(e),c=document.createElement("td");c.className="dtk-ui-dotcell",c.innerHTML=`<span class="dtk-ui-dot" style="background:${a.color}" title="${k(a.title)}"></span>`,o.appendChild(c);let v=document.createElement("td");v.className="dtk-ui-namecell";let w=e.error?`<span class="dtk-ui-err-badge" title="${k(e.error)}">!</span>`:"";v.title=Ze(e),v.innerHTML=`<span class="dtk-ui-name">${k(e.name)}</span>${w}${Qe(e.tags)}`,o.appendChild(v);let p=document.createElement("td");p.innerHTML=`<span class="dtk-ui-interval">${k(ke(e.interval_seconds))}</span>`,o.appendChild(p);let m=document.createElement("td");if(m.className="dtk-ui-sparkcell",e.pending)m.innerHTML='<span class="dtk-ui-spark-loading">loading\u2026</span>';else if(e.spark.length===0)m.innerHTML='<span class="dtk-ui-spark-empty">no data yet</span>';else{let f=document.createElement("canvas");f.className="dtk-spark",m.appendChild(f),t.push({canvas:f,points:e.spark.map(([S,E])=>({t:S,v:E})),anoms:e.spark_anoms})}o.appendChild(m);let h=document.createElement("td");if(h.className="dtk-ui-alertscell",e.pending)h.innerHTML=ue;else{let f=e.quality!==null&&e.quality.fdr!==null&&e.quality.fdr>e.budget,S="dtk-ui-alerts-n"+(e.alerts.anomaly>0?" hasany":"")+(f?" overbudget":""),E=e.alerts.per_day!==null?`<span class="dtk-ui-alerts-sub">\xB7 ${k(V(e.alerts.per_day))}</span>`:"";h.innerHTML=`<span class="${S}">${e.alerts.anomaly}</span>${E}`}o.appendChild(h);let g=document.createElement("td");e.pending?g.innerHTML=ue:e.alerts.last_ts!==null?g.innerHTML=`<span class="dtk-ui-lastalert" title="${k(me(e.alerts.last_ts))} UTC">${k(le(n,e.alerts.last_ts))}</span>`:g.innerHTML='<span class="dtk-ui-lastalert">\u2014</span>',o.appendChild(g);let s=document.createElement("td");s.innerHTML=e.pending?ue:`<span class="dtk-ui-rate">${k(F(e.anomaly_rate))}</span>`,o.appendChild(s);let b=document.createElement("td");b.innerHTML=e.pending?ue:Ve(e),o.appendChild(b);let x=document.createElement("td");x.innerHTML=e.locked?'<span class="dtk-ui-lock" title="pipeline lock currently held for this metric">LOCK</span>':"",o.appendChild(x);let M=document.createElement("td");M.className="dtk-ui-actionscell";let u=document.createElement("button");u.type="button",u.className="dtk-ui-actionbtn",u.textContent="Open",u.onclick=()=>r.onOpen(e.name);let $=document.createElement("button");$.type="button",$.className="dtk-ui-actionbtn",$.textContent="Tune",$.onclick=()=>r.onTune(e.name);let l=document.createElement("button");return l.type="button",l.className="dtk-ui-actionbtn",l.textContent="Run",l.onclick=()=>r.onRun(e.name),M.append(u,$,l),o.appendChild(M),o}function Ae(e,n){var r;switch(n){case"alerts":return e.alerts.anomaly;case"name":return e.name.toLowerCase();case"rate":return(r=e.anomaly_rate)!=null?r:-1;case"freshness":return Je(e).rank}}function tt(e,n){let r=e.filter(a=>a.enabled),t=e.filter(a=>!a.enabled),o=n.dir==="asc"?1:-1;return r.sort((a,c)=>{let v=Ae(a,n.key),w=Ae(c,n.key);return v<w?-1*o:v>w?1*o:a.name.localeCompare(c.name)}),t.sort((a,c)=>a.name.localeCompare(c.name)),[...r,...t]}var nt=[{label:"\u25CF",key:"freshness"},{label:"Name",key:"name"},{label:"Interval",key:null},{label:"Trend",key:null},{label:"Alerts",key:"alerts"},{label:"Last alert",key:null},{label:"Rate",key:"rate"},{label:"Quality",key:null},{label:"",key:null},{label:"",key:null}];function ot(e,n){let r=document.createElement("tr");for(let t of nt){let o=document.createElement("th");if(t.key){o.className="dtk-ui-th";let a=e.key===t.key?`<span class="dtk-ui-th-arrow">${e.dir==="asc"?"\u25B5":"\u25BE"}</span>`:"";o.innerHTML=`${k(t.label)}${a}`,o.onclick=()=>n.onSortChange(t.key)}else o.textContent=t.label;r.appendChild(o)}return r}function rt(e){return e===""?"metrics/":`metrics/${e}/`}function Be(e,n,r,t){var w;let o=[],a=document.createElement("div");if(a.className="dtk-ui-table-wrap",e.length===0)return a.innerHTML='<div class="dtk-ui-empty">No metrics match the current filter.</div>',{el:a,paint:()=>{}};let c=xe(e,p=>p.dir),v=[...c.keys()].sort((p,m)=>p===m?0:p===""?-1:m===""?1:p.localeCompare(m));for(let p of v){let m=(w=c.get(p))!=null?w:[],h=document.createElement("div");h.className="dtk-ui-group";let g=m.reduce((u,$)=>u+$.alerts.anomaly,0),s=document.createElement("div");s.className="dtk-ui-group-head",s.innerHTML=`<span class="dtk-ui-group-name">${k(rt(p))}</span><span class="dtk-ui-group-sub">${m.length} metric${m.length===1?"":"s"} \xB7 ${g} alert${g===1?"":"s"}</span>`,h.appendChild(s);let b=document.createElement("table");b.className="dtk-ui-table";let x=document.createElement("thead");x.appendChild(ot(n,t)),b.appendChild(x);let M=document.createElement("tbody");for(let u of tt(m,n))M.appendChild(et(u,r,t,o));b.appendChild(M),h.appendChild(b),a.appendChild(h)}return{el:a,paint:()=>{for(let p of o)Oe(p.canvas,p.points,p.anoms)}}}function Ie(e,n,r,t){let o=document.createElement("div");o.className="dtk-ui-overlay";let a=document.createElement("div");a.className="dtk-ui-overlay-modal",o.appendChild(a);let c=document.createElement("div");c.className="dtk-ui-overlay-head",c.innerHTML=`<span><span class="dtk-ui-overlay-title">${k(n)}</span><span class="dtk-ui-overlay-sub">window: ${k(r)}</span></span>`;let v=document.createElement("div");v.className="dtk-ui-overlay-actions";let w=document.createElement("button");w.type="button",w.className="dtk-ui-btn",w.textContent="Tune",w.onclick=()=>t.onTune(n);let p=document.createElement("button");p.type="button",p.className="dtk-ui-overlay-close",p.textContent="\u2715",p.title="Close (Esc)",v.append(w,p),c.appendChild(v),a.appendChild(c);let m=document.createElement("div");m.className="dtk-ui-overlay-body";let h=document.createElement("iframe");h.src=be(n,r),h.title=`detectkit report \u2014 ${n}`,m.appendChild(h),a.appendChild(m),e.appendChild(o);function g(){document.removeEventListener("keydown",s),o.remove(),t.onClose()}function s(b){b.key==="Escape"&&g()}return o.addEventListener("click",b=>{b.target===o&&g()}),p.onclick=g,document.addEventListener("keydown",s),{setWindow(b){let x=c.querySelector(".dtk-ui-overlay-sub");x&&(x.textContent=`window: ${b}`),h.src=be(n,b)},close:g}}var ze=/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/,Fe="dtk-ui-run-select-options";function qe(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Run pipeline</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let v=document.createElement("div");v.className="dtk-ui-field";let w=document.createElement("datalist");w.id=Fe;let p=document.createElement("input");p.type="text",p.className="dtk-ui-input",p.placeholder="metric name, tag:x, glob, or *",p.value="*",p.setAttribute("list",Fe),v.innerHTML='<span class="dtk-ui-field-label">Select</span>',v.append(p,w),c.appendChild(v);let m=document.createElement("div");m.className="dtk-ui-field",m.innerHTML='<span class="dtk-ui-field-label">Steps</span>';let h=document.createElement("div");h.className="dtk-ui-checks";let g={};for(let T of["load","detect","alert"]){let L=document.createElement("label");L.className="dtk-ui-check";let N=document.createElement("input");N.type="checkbox",N.checked=!0,N.onchange=H,L.append(N,document.createTextNode(T)),h.appendChild(L),g[T]=N}m.appendChild(h),c.appendChild(m);let s=document.createElement("div");s.className="dtk-ui-row2";let b=document.createElement("div");b.className="dtk-ui-field",b.innerHTML='<span class="dtk-ui-field-label">From</span>';let x=document.createElement("input");x.type="text",x.className="dtk-ui-input",x.placeholder="YYYY-MM-DD [HH:MM:SS]",x.oninput=H,b.appendChild(x);let M=document.createElement("div");M.className="dtk-ui-field",M.innerHTML='<span class="dtk-ui-field-label">To</span>';let u=document.createElement("input");u.type="text",u.className="dtk-ui-input",u.placeholder="YYYY-MM-DD [HH:MM:SS]",u.oninput=H,M.appendChild(u),s.append(b,M),c.appendChild(s);let $=document.createElement("div");$.className="dtk-ui-checks";let l=document.createElement("label");l.className="dtk-ui-check";let f=document.createElement("input");f.type="checkbox",l.append(f,document.createTextNode("force (skip lock check)"));let S=document.createElement("label");S.className="dtk-ui-check";let E=document.createElement("input");E.type="checkbox",S.append(E,document.createTextNode("full refresh")),$.append(l,S),c.appendChild($);let R=document.createElement("div");R.className="dtk-ui-btnrow";let P=document.createElement("button");P.type="button",P.className="dtk-ui-btn primary",P.textContent="Run";let O=document.createElement("button");O.type="button",O.className="dtk-ui-btn",O.textContent="Autotune";let _=document.createElement("button");_.type="button",_.className="dtk-ui-btn danger",_.textContent="Unlock",R.append(P,O,_),c.appendChild(R);let D=document.createElement("div");D.className="dtk-ui-reason",c.appendChild(D);let I=document.createElement("div");I.className="dtk-ui-field",I.innerHTML='<span class="dtk-ui-field-label">Log</span>';let A=document.createElement("div");A.className="dtk-ui-log";let Z=document.createElement("div");Z.className="dtk-ui-log-body";let z=document.createElement("div");z.className="dtk-ui-log-line",A.append(Z,z),I.appendChild(A),c.appendChild(I);let q=[];function ee(){Z.innerHTML=q.length===0?'<span class="dtk-ui-log-empty">no output yet</span>':q.map(k).join("<br>")}ee();function G(){return A.scrollTop+A.clientHeight>=A.scrollHeight-24}function te(){return Object.keys(g).filter(T=>g[T].checked)}function H(){let T=n.isPipelineBusy(),L=p.value.trim(),N=x.value.trim()===""||ze.test(x.value.trim()),i=u.value.trim()===""||ze.test(u.value.trim()),d="";T.busy?d=T.reason:L===""?d="select is required":!N||!i?d="from/to must be YYYY-MM-DD or YYYY-MM-DD HH:MM:SS":te().length===0&&(d="pick at least one step to run"),D.textContent=d,P.disabled=d!=="",O.disabled=T.busy||L===""||!N||!i,_.disabled=T.busy||L===""}function ie(){return{select:p.value.trim(),steps:te(),from:x.value.trim()||null,to:u.value.trim()||null,full_refresh:E.checked,force:f.checked}}P.onclick=()=>n.submitRun(ie()),O.onclick=()=>n.submitAutotune({select:p.value.trim(),from:x.value.trim()||null,to:u.value.trim()||null}),_.onclick=()=>{let T=p.value.trim();window.confirm(`Unlock the pipeline lock for "${T}"? Only do this if you're sure no dtk process is actually running against it.`)&&n.submitUnlock({select:T})};function ne(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=ne,r.onclick=ne,e.append(r,t),{el:t,open(T){T&&(p.value=T),r.classList.add("open"),t.classList.add("open"),H()},close:ne,isOpen(){return t.classList.contains("open")},refreshOptions(){w.innerHTML=["*",...n.getSelectOptions()].map(T=>`<option value="${k(T)}"></option>`).join("")},refreshBusyState:H,resetLog(){q=[],ee(),z.textContent="",z.className="dtk-ui-log-line"},appendLog(T){if(T.length===0)return;let L=G();q.push(...T),ee(),L&&(A.scrollTop=A.scrollHeight)},setLogStatus(T,L){if(T==="running")return;let N=T==="done"&&L===0?"exit-ok":T==="stopped"?"exit-stop":"exit-fail";z.className=`dtk-ui-log-line ${N}`,z.textContent=`\u2500\u2500 ${T} (exit ${L!=null?L:"?"}) \u2500\u2500`}}}function at(e){return e==="done"?"var(--st-recovery)":e==="failed"?"var(--st-anomaly)":e==="running"?"var(--clay)":"var(--faint)"}function Ue(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Jobs</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let v=document.createElement("div");v.className="dtk-ui-joblist",c.appendChild(v);function w(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=w,r.onclick=w,e.append(r,t),{el:t,open(){r.classList.add("open"),t.classList.add("open")},close:w,isOpen(){return t.classList.contains("open")},render(p,m,h){var g;if(p.length===0){v.innerHTML='<div class="dtk-ui-empty">No jobs yet.</div>';return}v.innerHTML="";for(let s of p){let b=document.createElement("div");b.className="dtk-ui-jobrow"+(s.id===h?" active":"");let x=s.status==="running"?" pulse":"",M=he(s.started_at,(g=s.finished_at)!=null?g:m),u=document.createElement("div");u.className="dtk-ui-jobrow-top",u.innerHTML=`<span class="dtk-ui-jobrow-status"><span class="dtk-ui-jobrow-dot${x}" style="background:${at(s.status)}"></span>${k(s.kind)} \xB7 ${k(s.status)}</span><span class="dtk-ui-jobrow-meta">${k(le(m,s.started_at))} \xB7 ${k(M)}</span>`,b.appendChild(u);let $=document.createElement("div");$.className="dtk-ui-jobrow-label",$.textContent=s.label,b.appendChild($);let l=document.createElement("div");if(l.className="dtk-ui-jobrow-actions",s.status==="running"){let f=document.createElement("button");f.type="button",f.className="dtk-ui-actionbtn",f.textContent="Stop",f.onclick=S=>{S.stopPropagation(),n.onStop(s.id)},l.appendChild(f)}if(s.kind==="tune"&&s.url){let f=document.createElement("a");f.className="dtk-ui-joblink",f.href=s.url,f.target="_blank",f.rel="noopener",f.textContent="Open tuner",f.onclick=S=>S.stopPropagation(),l.appendChild(f)}l.childElementCount>0&&b.appendChild(l),b.onclick=()=>n.onFollow(s.id),v.appendChild(b)}}}}var it=3,st=[{value:"24h",label:"24h"},{value:"7d",label:"7d"},{value:"30d",label:"30d"},{value:"90d",label:"90d"},{value:"all",label:"All"}];function lt(e,n){Ne(),n.classList.add(Y),n.innerHTML="";let r=document.createElement("div");r.className=`${Y}-root`,n.appendChild(r);let t={windowPreset:e.initial_window||"30d",metrics:e.metrics.map(pe),jobs:[],followedJobId:null,followOffset:0,tagFilter:null,sort:{key:"alerts",dir:ve.alerts}},o=null,a,c;function v(){let i=new Set,d=new Set;for(let C of e.metrics){i.add(C.name);for(let j of C.tags)d.add(j)}return[...i,...[...d].map(C=>`tag:${C}`)]}function w(){let i=t.jobs.find(d=>d.status==="running"&&d.kind!=="tune");return i?{busy:!0,reason:`a pipeline job is already running (${i.label})`}:{busy:!1,reason:""}}let p=document.createElement("div");p.className="dtk-ui-header",r.appendChild(p);let m=document.createElement("div");m.className="dtk-ui-brand",m.innerHTML=`<span class="dtk-ui-brand-dot"></span><span class="dtk-ui-brand-name">detectkit \xB7 <b>${k(e.project)}</b></span>`,p.appendChild(m);let h=document.createElement("div");h.className="dtk-ui-header-right",p.appendChild(h);let g=document.createElement("div");g.className="dtk-ui-seg";for(let i of st){let d=document.createElement("button");d.type="button",d.className="dtk-ui-seg-btn"+(t.windowPreset===i.value?" on":""),d.textContent=i.label,d.onclick=()=>{t.windowPreset!==i.value&&(t.windowPreset=i.value,g.querySelectorAll(".dtk-ui-seg-btn").forEach(C=>C.classList.remove("on")),d.classList.add("on"),o&&o.setWindow(i.value),N())},g.appendChild(d)}h.appendChild(g);let s=document.createElement("button");s.type="button",s.className="dtk-ui-iconbtn",s.title="Refresh overview",s.textContent="\u27F3",s.onclick=()=>{N()},h.appendChild(s);let b=document.createElement("button");b.type="button",b.className="dtk-ui-runbtn",b.textContent="Run pipeline",b.onclick=()=>P(),h.appendChild(b);let x=document.createElement("span");x.className="dtk-ui-progresschip",x.style.display="none",h.appendChild(x);function M(i,d){if(d===0||i>=d){x.style.display="none";return}x.textContent=`${i}/${d}`,x.style.display=""}let u=document.createElement("button");u.type="button",u.className="dtk-ui-jobschip",u.innerHTML='<span class="dtk-ui-jobschip-dot"></span><span>idle</span>',u.onclick=()=>{R.isOpen()?R.close():O()},h.appendChild(u);function $(){let i=t.jobs.find(j=>j.status==="running");u.classList.toggle("running",!!i);let d=i?`${i.kind} ${i.label}`:"idle",C=u.querySelector("span:last-child");C&&(C.textContent=d),u.title=i?`Started ${new Date(i.started_at).toLocaleString()}`:"No jobs running"}let l=document.createElement("div");l.className="dtk-ui-content",r.appendChild(l);function f(){var Q,U;l.innerHTML="";let i=e.metrics.length;if(i===0){let y=document.createElement("div");y.className="dtk-ui-empty",y.textContent="No metrics found for this project/selector.",l.appendChild(y);return}let d=t.metrics.filter(y=>!y.pending),C=d.filter(y=>y.error!==null);if(d.length===i&&C.length===i){let y=document.createElement("div");y.className="dtk-ui-banner";let se=(U=(Q=C[0])==null?void 0:Q.error)!=null?U:"unknown error";y.innerHTML=`<span>Failed to load overview: every metric failed (${k(se)}).</span>`;let J=document.createElement("button");J.type="button",J.className="dtk-ui-banner-retry",J.textContent="Retry",J.onclick=()=>{N()},y.appendChild(J),l.appendChild(y)}l.appendChild(Pe(t.metrics)),l.appendChild(De(d,t.tagFilter,y=>{t.tagFilter=y,f()}));let j=t.tagFilter===null?t.metrics:t.metrics.filter(y=>t.tagFilter===ae?y.tags.length===0:y.tags.includes(t.tagFilter)),X=Be(j,t.sort,Date.now(),{onOpen:S,onTune:y=>{q(y)},onRun:y=>P(y),onSortChange:y=>{t.sort=t.sort.key===y?{key:y,dir:t.sort.dir==="asc"?"desc":"asc"}:{key:y,dir:ve[y]},f()}});l.appendChild(X.el),X.paint()}function S(i){o&&o.close(),o=Ie(r,i,t.windowPreset,{onTune:d=>{q(d)},onClose:()=>{o=null}})}let E=qe(r,{submitRun:i=>{A(i)},submitAutotune:i=>{Z(i)},submitUnlock:i=>{z(i)},getSelectOptions:v,isPipelineBusy:w}),R=Ue(r,{onFollow:i=>G(i),onStop:i=>{ee(i)}});function P(i){R.close(),E.refreshOptions(),E.refreshBusyState(),E.open(i)}function O(){E.close(),R.render(t.jobs,Date.now(),t.followedJobId),R.open(),ie()}function _(i,d,C,j){let X={id:C,kind:i,label:d,status:"running",returncode:null,url:j,started_at:Date.now(),finished_at:null};t.jobs=[X,...t.jobs.filter(Q=>Q.id!==C)],te(),ie(),H()}let D=!1,I=new Set;async function A(i){if(!D){D=!0;try{let d=await Me(i);_("run",`run --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function Z(i){if(!D){D=!0;try{let d=await Se(i);_("autotune",`autotune --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function z(i){if(!D){D=!0;try{let d=await Ee(i);_("unlock",`unlock --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function q(i){if(!I.has(i)){I.add(i),B(r,"info",`Opening tuner for ${i}\u2026`);try{let d=await $e({metric:i});_("tune",`tune --select ${i}`,d.job_id,d.url),window.open(d.url,"_blank")}catch(d){B(r,"error",d.message)}finally{I.delete(i)}}}async function ee(i){try{await Le(i),B(r,"info","Stop requested."),H()}catch(d){B(r,"error",d.message)}}function G(i){t.followedJobId=i,t.followOffset=0,P(),E.resetLog(),T(i)}function te(){$(),R.render(t.jobs,Date.now(),t.followedJobId),E.refreshBusyState()}async function H(){try{let i=await Te();t.jobs=i.jobs}catch{}te()}function ie(){if(a!==void 0)return;let i=()=>{H().then(()=>{a=R.isOpen()||t.jobs.some(C=>C.status==="running")?window.setTimeout(i,2e3):void 0})};a=window.setTimeout(i,2e3)}function ne(){c!==void 0&&(window.clearTimeout(c),c=void 0)}function T(i){ne();let d=()=>{Ce(i,t.followOffset).then(C=>{if(t.followedJobId===i){if(E.appendLog(C.lines),t.followOffset=C.next_offset,C.status!=="running"){E.setLogStatus(C.status,C.returncode),c=void 0,H();return}c=window.setTimeout(d,1e3)}}).catch(C=>{B(r,"error",`job ${i}: ${C.message}`),c=void 0})};c=window.setTimeout(d,0)}let L=0;async function N(){let i=++L,d=t.windowPreset;if(t.metrics=e.metrics.map(pe),s.classList.add("spinning"),M(0,e.metrics.length),f(),e.metrics.length===0){s.classList.remove("spinning"),M(0,0);return}let C=[...e.metrics],j=0;async function X(){for(;;){if(i!==L)return;let U=C.shift();if(!U)return;let y;try{y=await we(U.name,d),y.pending=!1}catch(J){y={...pe(U),pending:!1,error:J.message}}if(i!==L)return;let se=t.metrics.findIndex(J=>J.name===U.name);se!==-1&&(t.metrics[se]=y),j++,M(j,e.metrics.length),f()}}let Q=Math.min(it,e.metrics.length);await Promise.all(Array.from({length:Q},()=>X())),i===L&&(s.classList.remove("spinning"),M(j,e.metrics.length),E.refreshOptions())}f(),E.refreshOptions(),N(),H()}window.__DTK_UI__={render:lt};})();
255
+ file: ${e.file}`}function et(e,n,r,t){let o=document.createElement("tr");o.className="dtk-ui-row"+(e.enabled?"":" disabled")+(e.error?" errored":"")+(e.pending?" pending":"");let a=Je(e),c=document.createElement("td");c.className="dtk-ui-dotcell",c.innerHTML=`<span class="dtk-ui-dot" style="background:${a.color}" title="${k(a.title)}"></span>`,o.appendChild(c);let h=document.createElement("td");h.className="dtk-ui-namecell";let w=e.error?`<span class="dtk-ui-err-badge" title="${k(e.error)}">!</span>`:"";h.title=Ze(e),h.innerHTML=`<span class="dtk-ui-name">${k(e.name)}</span>${w}${Qe(e.tags)}`,o.appendChild(h);let p=document.createElement("td");p.innerHTML=`<span class="dtk-ui-interval">${k(ke(e.interval_seconds))}</span>`,o.appendChild(p);let m=document.createElement("td");if(m.className="dtk-ui-sparkcell",e.pending)m.innerHTML='<span class="dtk-ui-spark-loading">loading\u2026</span>';else if(e.spark.length===0)m.innerHTML='<span class="dtk-ui-spark-empty">no data yet</span>';else{let f=document.createElement("canvas");f.className="dtk-spark",m.appendChild(f),t.push({canvas:f,points:e.spark.map(([S,E])=>({t:S,v:E})),anoms:e.spark_anoms})}o.appendChild(m);let x=document.createElement("td");if(x.className="dtk-ui-alertscell",e.pending)x.innerHTML=ue;else{let f=e.quality!==null&&e.quality.fdr!==null&&e.quality.fdr>e.budget,S="dtk-ui-alerts-n"+(e.alerts.anomaly>0?" hasany":"")+(f?" overbudget":""),E=e.alerts.per_day!==null?`<span class="dtk-ui-alerts-sub">\xB7 ${k(V(e.alerts.per_day))}</span>`:"";x.innerHTML=`<span class="${S}">${e.alerts.anomaly}</span>${E}`}o.appendChild(x);let b=document.createElement("td");e.pending?b.innerHTML=ue:e.alerts.last_ts!==null?b.innerHTML=`<span class="dtk-ui-lastalert" title="${k(me(e.alerts.last_ts))} UTC">${k(le(n,e.alerts.last_ts))}</span>`:b.innerHTML='<span class="dtk-ui-lastalert">\u2014</span>',o.appendChild(b);let s=document.createElement("td");s.innerHTML=e.pending?ue:`<span class="dtk-ui-rate">${k(F(e.anomaly_rate))}</span>`,o.appendChild(s);let g=document.createElement("td");g.innerHTML=e.pending?ue:Ve(e),o.appendChild(g);let v=document.createElement("td");v.innerHTML=e.locked?'<span class="dtk-ui-lock" title="pipeline lock currently held for this metric">LOCK</span>':"",o.appendChild(v);let T=document.createElement("td");T.className="dtk-ui-actionscell";let u=document.createElement("button");u.type="button",u.className="dtk-ui-actionbtn",u.textContent="Open",u.onclick=()=>r.onOpen(e.name);let L=document.createElement("button");L.type="button",L.className="dtk-ui-actionbtn",L.textContent="Tune",L.onclick=()=>r.onTune(e.name);let l=document.createElement("button");return l.type="button",l.className="dtk-ui-actionbtn",l.textContent="Run",l.onclick=()=>r.onRun(e.name),T.append(u,L,l),o.appendChild(T),o}function Ae(e,n){var r;switch(n){case"alerts":return e.alerts.anomaly;case"name":return e.name.toLowerCase();case"rate":return(r=e.anomaly_rate)!=null?r:-1;case"freshness":return Je(e).rank}}function tt(e,n){let r=e.filter(a=>a.enabled),t=e.filter(a=>!a.enabled),o=n.dir==="asc"?1:-1;return r.sort((a,c)=>{let h=Ae(a,n.key),w=Ae(c,n.key);return h<w?-1*o:h>w?1*o:a.name.localeCompare(c.name)}),t.sort((a,c)=>a.name.localeCompare(c.name)),[...r,...t]}var nt=[{label:"\u25CF",key:"freshness"},{label:"Name",key:"name"},{label:"Interval",key:null},{label:"Trend",key:null},{label:"Alerts",key:"alerts"},{label:"Last alert",key:null},{label:"Rate",key:"rate"},{label:"Quality",key:null},{label:"",key:null},{label:"",key:null}];function ot(e,n){let r=document.createElement("tr");for(let t of nt){let o=document.createElement("th");if(t.key){o.className="dtk-ui-th";let a=e.key===t.key?`<span class="dtk-ui-th-arrow">${e.dir==="asc"?"\u25B5":"\u25BE"}</span>`:"";o.innerHTML=`${k(t.label)}${a}`,o.onclick=()=>n.onSortChange(t.key)}else o.textContent=t.label;r.appendChild(o)}return r}function rt(e){return e===""?"metrics/":`metrics/${e}/`}function Be(e,n,r,t){var w;let o=[],a=document.createElement("div");if(a.className="dtk-ui-table-wrap",e.length===0)return a.innerHTML='<div class="dtk-ui-empty">No metrics match the current filter.</div>',{el:a,paint:()=>{}};let c=xe(e,p=>p.dir),h=[...c.keys()].sort((p,m)=>p===m?0:p===""?-1:m===""?1:p.localeCompare(m));for(let p of h){let m=(w=c.get(p))!=null?w:[],x=document.createElement("div");x.className="dtk-ui-group";let b=m.reduce((u,L)=>u+L.alerts.anomaly,0),s=document.createElement("div");s.className="dtk-ui-group-head",s.innerHTML=`<span class="dtk-ui-group-name">${k(rt(p))}</span><span class="dtk-ui-group-sub">${m.length} metric${m.length===1?"":"s"} \xB7 ${b} alert${b===1?"":"s"}</span>`,x.appendChild(s);let g=document.createElement("table");g.className="dtk-ui-table";let v=document.createElement("thead");v.appendChild(ot(n,t)),g.appendChild(v);let T=document.createElement("tbody");for(let u of tt(m,n))T.appendChild(et(u,r,t,o));g.appendChild(T),x.appendChild(g),a.appendChild(x)}return{el:a,paint:()=>{for(let p of o)Oe(p.canvas,p.points,p.anoms)}}}function Ie(e,n,r,t){let o=document.createElement("div");o.className="dtk-ui-overlay";let a=document.createElement("div");a.className="dtk-ui-overlay-modal",o.appendChild(a);let c=document.createElement("div");c.className="dtk-ui-overlay-head",c.innerHTML=`<span><span class="dtk-ui-overlay-title">${k(n)}</span><span class="dtk-ui-overlay-sub">window: ${k(r)}</span></span>`;let h=document.createElement("div");h.className="dtk-ui-overlay-actions";let w=document.createElement("button");w.type="button",w.className="dtk-ui-btn",w.textContent="Tune",w.onclick=()=>t.onTune(n);let p=document.createElement("button");p.type="button",p.className="dtk-ui-overlay-close",p.textContent="\u2715",p.title="Close (Esc)",h.append(w,p),c.appendChild(h),a.appendChild(c);let m=document.createElement("div");m.className="dtk-ui-overlay-body";let x=document.createElement("div");x.className="dtk-ui-overlay-loading",x.innerHTML=`<span class="dtk-ui-overlay-spinner"></span><span>Building the report for <b>${k(n)}</b>\u2026</span>`,m.appendChild(x);let b=document.createElement("iframe");b.title=`detectkit report \u2014 ${n}`,b.style.visibility="hidden",b.addEventListener("load",()=>{x.style.display="none",b.style.visibility="visible"}),b.src=be(n,r),m.appendChild(b),a.appendChild(m),e.appendChild(o);function s(){document.removeEventListener("keydown",g),o.remove(),t.onClose()}function g(v){v.key==="Escape"&&s()}return o.addEventListener("click",v=>{v.target===o&&s()}),p.onclick=s,document.addEventListener("keydown",g),{setWindow(v){let T=c.querySelector(".dtk-ui-overlay-sub");T&&(T.textContent=`window: ${v}`),x.style.display="",b.style.visibility="hidden",b.src=be(n,v)},close:s}}var ze=/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/,Fe="dtk-ui-run-select-options";function qe(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Run pipeline</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let h=document.createElement("div");h.className="dtk-ui-field";let w=document.createElement("datalist");w.id=Fe;let p=document.createElement("input");p.type="text",p.className="dtk-ui-input",p.placeholder="metric name, tag:x, glob, or *",p.value="*",p.setAttribute("list",Fe),h.innerHTML='<span class="dtk-ui-field-label">Select</span>',h.append(p,w),c.appendChild(h);let m=document.createElement("div");m.className="dtk-ui-field",m.innerHTML='<span class="dtk-ui-field-label">Steps</span>';let x=document.createElement("div");x.className="dtk-ui-checks";let b={};for(let C of["load","detect","alert"]){let $=document.createElement("label");$.className="dtk-ui-check";let N=document.createElement("input");N.type="checkbox",N.checked=!0,N.onchange=H,$.append(N,document.createTextNode(C)),x.appendChild($),b[C]=N}m.appendChild(x),c.appendChild(m);let s=document.createElement("div");s.className="dtk-ui-row2";let g=document.createElement("div");g.className="dtk-ui-field",g.innerHTML='<span class="dtk-ui-field-label">From</span>';let v=document.createElement("input");v.type="text",v.className="dtk-ui-input",v.placeholder="YYYY-MM-DD [HH:MM:SS]",v.oninput=H,g.appendChild(v);let T=document.createElement("div");T.className="dtk-ui-field",T.innerHTML='<span class="dtk-ui-field-label">To</span>';let u=document.createElement("input");u.type="text",u.className="dtk-ui-input",u.placeholder="YYYY-MM-DD [HH:MM:SS]",u.oninput=H,T.appendChild(u),s.append(g,T),c.appendChild(s);let L=document.createElement("div");L.className="dtk-ui-checks";let l=document.createElement("label");l.className="dtk-ui-check";let f=document.createElement("input");f.type="checkbox",l.append(f,document.createTextNode("force (skip lock check)"));let S=document.createElement("label");S.className="dtk-ui-check";let E=document.createElement("input");E.type="checkbox",S.append(E,document.createTextNode("full refresh")),L.append(l,S),c.appendChild(L);let R=document.createElement("div");R.className="dtk-ui-btnrow";let P=document.createElement("button");P.type="button",P.className="dtk-ui-btn primary",P.textContent="Run";let O=document.createElement("button");O.type="button",O.className="dtk-ui-btn",O.textContent="Autotune";let _=document.createElement("button");_.type="button",_.className="dtk-ui-btn danger",_.textContent="Unlock",R.append(P,O,_),c.appendChild(R);let D=document.createElement("div");D.className="dtk-ui-reason",c.appendChild(D);let I=document.createElement("div");I.className="dtk-ui-field",I.innerHTML='<span class="dtk-ui-field-label">Log</span>';let A=document.createElement("div");A.className="dtk-ui-log";let Z=document.createElement("div");Z.className="dtk-ui-log-body";let z=document.createElement("div");z.className="dtk-ui-log-line",A.append(Z,z),I.appendChild(A),c.appendChild(I);let q=[];function ee(){Z.innerHTML=q.length===0?'<span class="dtk-ui-log-empty">no output yet</span>':q.map(k).join("<br>")}ee();function G(){return A.scrollTop+A.clientHeight>=A.scrollHeight-24}function te(){return Object.keys(b).filter(C=>b[C].checked)}function H(){let C=n.isPipelineBusy(),$=p.value.trim(),N=v.value.trim()===""||ze.test(v.value.trim()),i=u.value.trim()===""||ze.test(u.value.trim()),d="";C.busy?d=C.reason:$===""?d="select is required":!N||!i?d="from/to must be YYYY-MM-DD or YYYY-MM-DD HH:MM:SS":te().length===0&&(d="pick at least one step to run"),D.textContent=d,P.disabled=d!=="",O.disabled=C.busy||$===""||!N||!i,_.disabled=C.busy||$===""}function ie(){return{select:p.value.trim(),steps:te(),from:v.value.trim()||null,to:u.value.trim()||null,full_refresh:E.checked,force:f.checked}}P.onclick=()=>n.submitRun(ie()),O.onclick=()=>n.submitAutotune({select:p.value.trim(),from:v.value.trim()||null,to:u.value.trim()||null}),_.onclick=()=>{let C=p.value.trim();window.confirm(`Unlock the pipeline lock for "${C}"? Only do this if you're sure no dtk process is actually running against it.`)&&n.submitUnlock({select:C})};function ne(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=ne,r.onclick=ne,e.append(r,t),{el:t,open(C){C&&(p.value=C),r.classList.add("open"),t.classList.add("open"),H()},close:ne,isOpen(){return t.classList.contains("open")},refreshOptions(){w.innerHTML=["*",...n.getSelectOptions()].map(C=>`<option value="${k(C)}"></option>`).join("")},refreshBusyState:H,resetLog(){q=[],ee(),z.textContent="",z.className="dtk-ui-log-line"},appendLog(C){if(C.length===0)return;let $=G();q.push(...C),ee(),$&&(A.scrollTop=A.scrollHeight)},setLogStatus(C,$){if(C==="running")return;let N=C==="done"&&$===0?"exit-ok":C==="stopped"?"exit-stop":"exit-fail";z.className=`dtk-ui-log-line ${N}`,z.textContent=`\u2500\u2500 ${C} (exit ${$!=null?$:"?"}) \u2500\u2500`}}}function at(e){return e==="done"?"var(--st-recovery)":e==="failed"?"var(--st-anomaly)":e==="running"?"var(--clay)":"var(--faint)"}function Ue(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Jobs</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let h=document.createElement("div");h.className="dtk-ui-joblist",c.appendChild(h);function w(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=w,r.onclick=w,e.append(r,t),{el:t,open(){r.classList.add("open"),t.classList.add("open")},close:w,isOpen(){return t.classList.contains("open")},render(p,m,x){var b;if(p.length===0){h.innerHTML='<div class="dtk-ui-empty">No jobs yet.</div>';return}h.innerHTML="";for(let s of p){let g=document.createElement("div");g.className="dtk-ui-jobrow"+(s.id===x?" active":"");let v=s.status==="running"?" pulse":"",T=he(s.started_at,(b=s.finished_at)!=null?b:m),u=document.createElement("div");u.className="dtk-ui-jobrow-top",u.innerHTML=`<span class="dtk-ui-jobrow-status"><span class="dtk-ui-jobrow-dot${v}" style="background:${at(s.status)}"></span>${k(s.kind)} \xB7 ${k(s.status)}</span><span class="dtk-ui-jobrow-meta">${k(le(m,s.started_at))} \xB7 ${k(T)}</span>`,g.appendChild(u);let L=document.createElement("div");L.className="dtk-ui-jobrow-label",L.textContent=s.label,g.appendChild(L);let l=document.createElement("div");if(l.className="dtk-ui-jobrow-actions",s.status==="running"){let f=document.createElement("button");f.type="button",f.className="dtk-ui-actionbtn",f.textContent="Stop",f.onclick=S=>{S.stopPropagation(),n.onStop(s.id)},l.appendChild(f)}if(s.kind==="tune"&&s.url){let f=document.createElement("a");f.className="dtk-ui-joblink",f.href=s.url,f.target="_blank",f.rel="noopener",f.textContent="Open tuner",f.onclick=S=>S.stopPropagation(),l.appendChild(f)}l.childElementCount>0&&g.appendChild(l),g.onclick=()=>n.onFollow(s.id),h.appendChild(g)}}}}var it=3,st=[{value:"24h",label:"24h"},{value:"7d",label:"7d"},{value:"30d",label:"30d"},{value:"90d",label:"90d"},{value:"all",label:"All"}];function lt(e,n){Ne(),n.classList.add(Y),n.innerHTML="";let r=document.createElement("div");r.className=`${Y}-root`,n.appendChild(r);let t={windowPreset:e.initial_window||"30d",metrics:e.metrics.map(pe),jobs:[],followedJobId:null,followOffset:0,tagFilter:null,sort:{key:"alerts",dir:ve.alerts}},o=null,a,c;function h(){let i=new Set,d=new Set;for(let M of e.metrics){i.add(M.name);for(let j of M.tags)d.add(j)}return[...i,...[...d].map(M=>`tag:${M}`)]}function w(){let i=t.jobs.find(d=>d.status==="running"&&d.kind!=="tune");return i?{busy:!0,reason:`a pipeline job is already running (${i.label})`}:{busy:!1,reason:""}}let p=document.createElement("div");p.className="dtk-ui-header",r.appendChild(p);let m=document.createElement("div");m.className="dtk-ui-brand",m.innerHTML=`<span class="dtk-ui-brand-dot"></span><span class="dtk-ui-brand-name">detectkit \xB7 <b>${k(e.project)}</b></span>`,p.appendChild(m);let x=document.createElement("div");x.className="dtk-ui-header-right",p.appendChild(x);let b=document.createElement("div");b.className="dtk-ui-seg";for(let i of st){let d=document.createElement("button");d.type="button",d.className="dtk-ui-seg-btn"+(t.windowPreset===i.value?" on":""),d.textContent=i.label,d.onclick=()=>{t.windowPreset!==i.value&&(t.windowPreset=i.value,b.querySelectorAll(".dtk-ui-seg-btn").forEach(M=>M.classList.remove("on")),d.classList.add("on"),o&&o.setWindow(i.value),N())},b.appendChild(d)}x.appendChild(b);let s=document.createElement("button");s.type="button",s.className="dtk-ui-iconbtn",s.title="Refresh overview",s.textContent="\u27F3",s.onclick=()=>{N()},x.appendChild(s);let g=document.createElement("button");g.type="button",g.className="dtk-ui-runbtn",g.textContent="Run pipeline",g.onclick=()=>P(),x.appendChild(g);let v=document.createElement("span");v.className="dtk-ui-progresschip",v.style.display="none",x.appendChild(v);function T(i,d){if(d===0||i>=d){v.style.display="none";return}v.textContent=`${i}/${d}`,v.style.display=""}let u=document.createElement("button");u.type="button",u.className="dtk-ui-jobschip",u.innerHTML='<span class="dtk-ui-jobschip-dot"></span><span>idle</span>',u.onclick=()=>{R.isOpen()?R.close():O()},x.appendChild(u);function L(){let i=t.jobs.find(j=>j.status==="running");u.classList.toggle("running",!!i);let d=i?`${i.kind} ${i.label}`:"idle",M=u.querySelector("span:last-child");M&&(M.textContent=d),u.title=i?`Started ${new Date(i.started_at).toLocaleString()}`:"No jobs running"}let l=document.createElement("div");l.className="dtk-ui-content",r.appendChild(l);function f(){var Q,U;l.innerHTML="";let i=e.metrics.length;if(i===0){let y=document.createElement("div");y.className="dtk-ui-empty",y.textContent="No metrics found for this project/selector.",l.appendChild(y);return}let d=t.metrics.filter(y=>!y.pending),M=d.filter(y=>y.error!==null);if(d.length===i&&M.length===i){let y=document.createElement("div");y.className="dtk-ui-banner";let se=(U=(Q=M[0])==null?void 0:Q.error)!=null?U:"unknown error";y.innerHTML=`<span>Failed to load overview: every metric failed (${k(se)}).</span>`;let J=document.createElement("button");J.type="button",J.className="dtk-ui-banner-retry",J.textContent="Retry",J.onclick=()=>{N()},y.appendChild(J),l.appendChild(y)}l.appendChild(Pe(t.metrics)),l.appendChild(De(d,t.tagFilter,y=>{t.tagFilter=y,f()}));let j=t.tagFilter===null?t.metrics:t.metrics.filter(y=>t.tagFilter===ae?y.tags.length===0:y.tags.includes(t.tagFilter)),X=Be(j,t.sort,Date.now(),{onOpen:S,onTune:y=>{q(y)},onRun:y=>P(y),onSortChange:y=>{t.sort=t.sort.key===y?{key:y,dir:t.sort.dir==="asc"?"desc":"asc"}:{key:y,dir:ve[y]},f()}});l.appendChild(X.el),X.paint()}function S(i){o&&o.close(),o=Ie(r,i,t.windowPreset,{onTune:d=>{q(d)},onClose:()=>{o=null}})}let E=qe(r,{submitRun:i=>{A(i)},submitAutotune:i=>{Z(i)},submitUnlock:i=>{z(i)},getSelectOptions:h,isPipelineBusy:w}),R=Ue(r,{onFollow:i=>G(i),onStop:i=>{ee(i)}});function P(i){R.close(),E.refreshOptions(),E.refreshBusyState(),E.open(i)}function O(){E.close(),R.render(t.jobs,Date.now(),t.followedJobId),R.open(),ie()}function _(i,d,M,j){let X={id:M,kind:i,label:d,status:"running",returncode:null,url:j,started_at:Date.now(),finished_at:null};t.jobs=[X,...t.jobs.filter(Q=>Q.id!==M)],te(),ie(),H()}let D=!1,I=new Set;async function A(i){if(!D){D=!0;try{let d=await Me(i);_("run",`run --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function Z(i){if(!D){D=!0;try{let d=await Se(i);_("autotune",`autotune --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function z(i){if(!D){D=!0;try{let d=await Ee(i);_("unlock",`unlock --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function q(i){if(!I.has(i)){I.add(i),B(r,"info",`Opening tuner for ${i}\u2026`);try{let d=await Le({metric:i});_("tune",`tune --select ${i}`,d.job_id,d.url),window.open(d.url,"_blank")}catch(d){B(r,"error",d.message)}finally{I.delete(i)}}}async function ee(i){try{await $e(i),B(r,"info","Stop requested."),H()}catch(d){B(r,"error",d.message)}}function G(i){t.followedJobId=i,t.followOffset=0,P(),E.resetLog(),C(i)}function te(){L(),R.render(t.jobs,Date.now(),t.followedJobId),E.refreshBusyState()}async function H(){try{let i=await Te();t.jobs=i.jobs}catch{}te()}function ie(){if(a!==void 0)return;let i=()=>{H().then(()=>{a=R.isOpen()||t.jobs.some(M=>M.status==="running")?window.setTimeout(i,2e3):void 0})};a=window.setTimeout(i,2e3)}function ne(){c!==void 0&&(window.clearTimeout(c),c=void 0)}function C(i){ne();let d=()=>{Ce(i,t.followOffset).then(M=>{if(t.followedJobId===i){if(E.appendLog(M.lines),t.followOffset=M.next_offset,M.status!=="running"){E.setLogStatus(M.status,M.returncode),c=void 0,H();return}c=window.setTimeout(d,1e3)}}).catch(M=>{B(r,"error",`job ${i}: ${M.message}`),c=void 0})};c=window.setTimeout(d,0)}let $=0;async function N(){let i=++$,d=t.windowPreset;if(t.metrics=e.metrics.map(pe),s.classList.add("spinning"),T(0,e.metrics.length),f(),e.metrics.length===0){s.classList.remove("spinning"),T(0,0);return}let M=[...e.metrics],j=0;async function X(){for(;;){if(i!==$)return;let U=M.shift();if(!U)return;let y;try{y=await we(U.name,d),y.pending=!1}catch(J){y={...pe(U),pending:!1,error:J.message}}if(i!==$)return;let se=t.metrics.findIndex(J=>J.name===U.name);se!==-1&&(t.metrics[se]=y),j++,T(j,e.metrics.length),f()}}let Q=Math.min(it,e.metrics.length);await Promise.all(Array.from({length:Q},()=>X())),i===$&&(s.classList.remove("spinning"),T(j,e.metrics.length),E.refreshOptions())}f(),E.refreshOptions(),N(),H()}window.__DTK_UI__={render:lt};})();
@@ -519,8 +519,13 @@ class _Handler(BaseHTTPRequestHandler):
519
519
 
520
520
  Mirrors ``tuning/server.py``: the status line is latin-1 only, and an
521
521
  exception message can carry non-ASCII (e.g. an ``≈`` from a validation
522
- error), so the detail rides in the body instead.
522
+ error), so the detail rides in the body instead. Errors other than the
523
+ routine bad-token 403 are also echoed to the terminal — a page stuck on
524
+ a failing request should be diagnosable without opening devtools.
523
525
  """
526
+ if code >= 400 and code != 403:
527
+ first_line = detail.splitlines()[0] if detail else ""
528
+ self._srv().echo(f" [ui] {code} {urlparse(self.path).path}: {first_line}")
524
529
  body = detail.encode("utf-8")
525
530
  self.send_response(code)
526
531
  self.send_header("Content-Type", "text/plain; charset=utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.49.1
3
+ Version: 0.49.2
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes