detectkit 0.49.0__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 (153) hide show
  1. {detectkit-0.49.0/detectkit.egg-info → detectkit-0.49.2}/PKG-INFO +1 -1
  2. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/__init__.py +1 -1
  3. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_decision.py +2 -1
  4. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_replay.py +24 -7
  5. detectkit-0.49.2/detectkit/ui/assets/ui.js +255 -0
  6. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/overview.py +101 -6
  7. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/server.py +62 -4
  8. {detectkit-0.49.0 → detectkit-0.49.2/detectkit.egg-info}/PKG-INFO +1 -1
  9. detectkit-0.49.0/detectkit/ui/assets/ui.js +0 -243
  10. {detectkit-0.49.0 → detectkit-0.49.2}/LICENSE +0 -0
  11. {detectkit-0.49.0 → detectkit-0.49.2}/MANIFEST.in +0 -0
  12. {detectkit-0.49.0 → detectkit-0.49.2}/README.md +0 -0
  13. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/__init__.py +0 -0
  14. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/__init__.py +0 -0
  15. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/base.py +0 -0
  16. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/branding.py +0 -0
  17. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/email.py +0 -0
  18. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/factory.py +0 -0
  19. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/mattermost.py +0 -0
  20. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/slack.py +0 -0
  21. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/telegram.py +0 -0
  22. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/webhook.py +0 -0
  23. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/__init__.py +0 -0
  24. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_base.py +0 -0
  25. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  26. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  27. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  28. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_types.py +0 -0
  29. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  30. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/__init__.py +0 -0
  31. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/_base.py +0 -0
  32. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/_types.py +0 -0
  33. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/autotuner.py +0 -0
  34. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/config_emitter.py +0 -0
  35. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/crossval.py +0 -0
  36. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/detector_select.py +0 -0
  37. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/distribution.py +0 -0
  38. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/grid_search.py +0 -0
  39. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/labels.py +0 -0
  40. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/result.py +0 -0
  41. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/runner.py +0 -0
  42. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/scoring.py +0 -0
  43. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/seasonality_search.py +0 -0
  44. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/settings.py +0 -0
  45. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/window_select.py +0 -0
  46. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/__init__.py +0 -0
  47. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/_output.py +0 -0
  48. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  49. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  50. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  51. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  52. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  55. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/project.md +0 -0
  56. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  57. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  58. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  59. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  60. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  61. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/__init__.py +0 -0
  62. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/autotune.py +0 -0
  63. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/clean.py +0 -0
  64. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/init.py +0 -0
  65. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/init_claude.py +0 -0
  66. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/osi.py +0 -0
  67. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/run.py +0 -0
  68. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/test_alert.py +0 -0
  69. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/tune.py +0 -0
  70. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/ui.py +0 -0
  71. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/unlock.py +0 -0
  72. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/main.py +0 -0
  73. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/__init__.py +0 -0
  74. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/metric_config.py +0 -0
  75. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/profile.py +0 -0
  76. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/project_config.py +0 -0
  77. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/validator.py +0 -0
  78. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/__init__.py +0 -0
  79. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/interval.py +0 -0
  80. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/models.py +0 -0
  81. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/__init__.py +0 -0
  82. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/_sql_manager.py +0 -0
  83. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/clickhouse_manager.py +0 -0
  84. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/__init__.py +0 -0
  85. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_alert_states.py +0 -0
  86. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  87. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_base.py +0 -0
  88. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_datapoints.py +0 -0
  89. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_detections.py +0 -0
  90. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_maintenance.py +0 -0
  91. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_metrics.py +0 -0
  92. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_schema.py +0 -0
  93. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_tasks.py +0 -0
  94. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/manager.py +0 -0
  95. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/manager.py +0 -0
  96. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/mysql_manager.py +0 -0
  97. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/postgres_manager.py +0 -0
  98. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/tables.py +0 -0
  99. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/__init__.py +0 -0
  100. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/base.py +0 -0
  101. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/factory.py +0 -0
  102. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/seasonality.py +0 -0
  103. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/__init__.py +0 -0
  104. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/_windowed.py +0 -0
  105. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/iqr.py +0 -0
  106. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/mad.py +0 -0
  107. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  108. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/zscore.py +0 -0
  109. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/__init__.py +0 -0
  110. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/metric_loader.py +0 -0
  111. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/query_template.py +0 -0
  112. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/__init__.py +0 -0
  113. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/error_dispatch.py +0 -0
  114. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/__init__.py +0 -0
  115. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  116. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_base.py +0 -0
  117. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  118. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  119. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_types.py +0 -0
  120. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/manager.py +0 -0
  121. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/__init__.py +0 -0
  122. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/assets/report.js +0 -0
  123. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/builder.py +0 -0
  124. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/html_report.py +0 -0
  125. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/__init__.py +0 -0
  126. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/errors.py +0 -0
  127. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/exporter.py +0 -0
  128. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/importer.py +0 -0
  129. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/osi_model.py +0 -0
  130. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/query_gen.py +0 -0
  131. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/__init__.py +0 -0
  132. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/assets/tune.js +0 -0
  133. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/config_writer.py +0 -0
  134. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/html.py +0 -0
  135. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/payload.py +0 -0
  136. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/server.py +0 -0
  137. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/__init__.py +0 -0
  138. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/html.py +0 -0
  139. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/jobs.py +0 -0
  140. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/__init__.py +0 -0
  141. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/datetime_utils.py +0 -0
  142. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/env_interpolation.py +0 -0
  143. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/json_utils.py +0 -0
  144. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/stats.py +0 -0
  145. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/SOURCES.txt +0 -0
  146. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/dependency_links.txt +0 -0
  147. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/entry_points.txt +0 -0
  148. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/requires.txt +0 -0
  149. {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/top_level.txt +0 -0
  150. {detectkit-0.49.0 → detectkit-0.49.2}/pyproject.toml +0 -0
  151. {detectkit-0.49.0 → detectkit-0.49.2}/requirements.txt +0 -0
  152. {detectkit-0.49.0 → detectkit-0.49.2}/setup.cfg +0 -0
  153. {detectkit-0.49.0 → 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.0
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.0"
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
 
@@ -0,0 +1,255 @@
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
+ .${Y}{
3
+ --clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;
4
+ --muted:#6e675b;--faint:#9a9384;
5
+ --term-bg:#211e1a;--term-border:#332f29;--term-text:#c9c2b4;
6
+ --st-anomaly:#d63232;--st-recovery:#36a64f;--st-nodata:#f0ad4e;--st-error:#5a7a8c;
7
+ --accent-green:#2e9e73;
8
+ --sans:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
9
+ --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
10
+ /* dark-terminal local surfaces, derived from the term-* palette above */
11
+ --bg:var(--term-bg);--surface:#2a2620;--surface-2:#332e26;--border:var(--term-border);
12
+ --text:var(--term-text);--text-strong:var(--paper);
13
+ display:block;min-height:100vh;background:var(--bg);color:var(--text);
14
+ font-family:var(--sans);position:relative;
15
+ }
16
+ .${Y} *{box-sizing:border-box;}
17
+ .${Y} a{color:var(--clay);}
18
+ .${Y}-root{max-width:1400px;margin:0 auto;padding:16px 20px 56px;display:flex;
19
+ flex-direction:column;gap:16px;}
20
+ .dtk-ui-content{display:flex;flex-direction:column;gap:16px;}
21
+
22
+ /* --- header ------------------------------------------------------------- */
23
+ .dtk-ui-header{display:flex;align-items:center;justify-content:space-between;gap:14px;
24
+ flex-wrap:wrap;padding-bottom:2px;}
25
+ .dtk-ui-brand{display:flex;align-items:center;gap:9px;}
26
+ .dtk-ui-brand-dot{width:11px;height:11px;border-radius:3px;background:var(--clay);flex:0 0 auto;}
27
+ .dtk-ui-brand-name{font-family:var(--mono);font-size:14px;color:var(--text-strong);
28
+ letter-spacing:-0.01em;}
29
+ .dtk-ui-brand-name b{font-weight:700;}
30
+ .dtk-ui-header-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
31
+ .dtk-ui-seg{display:flex;gap:3px;background:var(--surface);border:1px solid var(--border);
32
+ border-radius:8px;padding:3px;}
33
+ .dtk-ui-seg-btn{border:0;background:transparent;color:var(--faint);font-family:var(--mono);
34
+ font-size:11.5px;padding:5px 10px;border-radius:6px;cursor:pointer;white-space:nowrap;}
35
+ .dtk-ui-seg-btn:hover{color:var(--text);}
36
+ .dtk-ui-seg-btn.on{background:var(--clay);color:#fff;font-weight:600;}
37
+ .dtk-ui-iconbtn{border:1px solid var(--border);background:var(--surface);color:var(--text);
38
+ border-radius:8px;width:32px;height:32px;display:inline-flex;align-items:center;
39
+ justify-content:center;cursor:pointer;font-size:14px;line-height:1;}
40
+ .dtk-ui-iconbtn:hover{border-color:var(--clay);color:#fff;background:var(--clay-700);}
41
+ .dtk-ui-iconbtn.spinning{animation:dtk-ui-spin 0.8s linear infinite;}
42
+ @keyframes dtk-ui-spin{to{transform:rotate(360deg);}}
43
+ .dtk-ui-runbtn{border:0;background:var(--clay);color:#fff;font-family:var(--sans);
44
+ font-size:13px;font-weight:600;padding:8px 15px;border-radius:8px;cursor:pointer;}
45
+ .dtk-ui-runbtn:hover{background:var(--clay-700);}
46
+ .dtk-ui-jobschip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--border);
47
+ background:var(--surface);color:var(--faint);font-family:var(--mono);font-size:11.5px;
48
+ padding:6px 12px;border-radius:999px;cursor:pointer;white-space:nowrap;}
49
+ .dtk-ui-jobschip:hover{border-color:var(--clay);}
50
+ .dtk-ui-jobschip-dot{width:7px;height:7px;border-radius:50%;background:var(--faint);flex:0 0 auto;}
51
+ .dtk-ui-jobschip.running .dtk-ui-jobschip-dot{background:var(--clay);
52
+ animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
53
+ .dtk-ui-jobschip.running{color:var(--text);border-color:var(--clay);}
54
+ @keyframes dtk-ui-pulse{0%,100%{opacity:1;}50%{opacity:0.35;}}
55
+ .dtk-ui-progresschip{font-family:var(--mono);font-size:11.5px;color:var(--faint);
56
+ white-space:nowrap;}
57
+
58
+ /* --- banner / empty ------------------------------------------------------ */
59
+ .dtk-ui-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;
60
+ background:rgba(214,50,50,0.1);border:1px solid rgba(214,50,50,0.4);border-radius:10px;
61
+ padding:11px 14px;color:var(--text);font-size:13px;}
62
+ .dtk-ui-banner-retry{border:1px solid var(--st-anomaly);background:transparent;color:var(--st-anomaly);
63
+ border-radius:7px;padding:6px 13px;font-family:var(--sans);font-size:12.5px;cursor:pointer;
64
+ flex:0 0 auto;}
65
+ .dtk-ui-banner-retry:hover{background:rgba(214,50,50,0.15);}
66
+ .dtk-ui-empty{padding:60px 20px;text-align:center;color:var(--faint);font-size:14px;}
67
+
68
+ /* --- stat tiles ----------------------------------------------------------- */
69
+ .dtk-ui-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;}
70
+ .dtk-ui-tile{background:var(--surface);border:1px solid var(--border);border-radius:10px;
71
+ padding:12px 14px;display:flex;flex-direction:column;gap:3px;}
72
+ .dtk-ui-tile-val{font-family:var(--mono);font-size:23px;font-weight:700;color:var(--text-strong);
73
+ line-height:1.1;}
74
+ .dtk-ui-tile-label{font-family:var(--mono);font-size:10.5px;color:var(--faint);
75
+ text-transform:uppercase;letter-spacing:0.06em;}
76
+ .dtk-ui-tile-sub{font-size:11.5px;color:var(--muted);font-family:var(--mono);}
77
+ .dtk-ui-tile-sub.warn{color:var(--st-nodata);font-weight:600;}
78
+ .dtk-ui-tile.err .dtk-ui-tile-val{color:var(--st-anomaly);}
79
+
80
+ /* --- tag rollup strip ------------------------------------------------------ */
81
+ .dtk-ui-tags{display:flex;flex-wrap:wrap;gap:7px;}
82
+ .dtk-ui-tag{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;background:var(--surface);
83
+ border:1px solid var(--border);border-radius:999px;font-size:11.5px;color:var(--muted);
84
+ cursor:pointer;font-family:var(--mono);}
85
+ .dtk-ui-tag:hover{border-color:var(--clay);color:var(--text);}
86
+ .dtk-ui-tag.on{background:var(--clay);border-color:var(--clay);color:#fff;}
87
+ .dtk-ui-tag-name{font-weight:600;}
88
+ .dtk-ui-tag.on .dtk-ui-tag-name{color:#fff;}
89
+ .dtk-ui-tag-n{color:var(--text);}
90
+ .dtk-ui-tag.on .dtk-ui-tag-n,.dtk-ui-tag.on .dtk-ui-tag-sub{color:rgba(255,255,255,0.85);}
91
+ .dtk-ui-tag-sub{color:var(--faint);}
92
+
93
+ /* --- metrics table --------------------------------------------------------- */
94
+ .dtk-ui-table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;
95
+ overflow:hidden;}
96
+ .dtk-ui-group + .dtk-ui-group{border-top:1px solid var(--border);}
97
+ .dtk-ui-group-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
98
+ padding:9px 14px;background:var(--surface-2);}
99
+ .dtk-ui-group-name{font-family:var(--mono);font-size:11.5px;color:var(--text);font-weight:600;}
100
+ .dtk-ui-group-sub{font-family:var(--mono);font-size:11px;color:var(--faint);}
101
+ .dtk-ui-table{width:100%;border-collapse:collapse;font-size:12.5px;}
102
+ .dtk-ui-table thead th{text-align:left;padding:8px 12px;font-family:var(--mono);font-size:10.5px;
103
+ color:var(--faint);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;
104
+ border-bottom:1px solid var(--border);white-space:nowrap;}
105
+ .dtk-ui-th{cursor:pointer;user-select:none;}
106
+ .dtk-ui-th:hover{color:var(--text);}
107
+ .dtk-ui-th-arrow{margin-left:5px;color:var(--clay);}
108
+ .dtk-ui-table td{padding:7px 12px;border-bottom:1px solid var(--border);vertical-align:middle;
109
+ color:var(--text);}
110
+ .dtk-ui-row:last-child td{border-bottom:0;}
111
+ .dtk-ui-row:hover{background:var(--surface-2);}
112
+ .dtk-ui-row.disabled{opacity:0.5;}
113
+ .dtk-ui-row.errored{background:rgba(214,50,50,0.05);}
114
+ .dtk-ui-row.pending{opacity:0.7;}
115
+ .dtk-ui-pending{font-family:var(--mono);color:var(--faint);}
116
+ .dtk-ui-dotcell{width:14px;}
117
+ .dtk-ui-dot{width:9px;height:9px;border-radius:50%;display:inline-block;cursor:help;}
118
+ .dtk-ui-namecell{min-width:170px;}
119
+ .dtk-ui-name{font-weight:600;color:var(--text-strong);}
120
+ .dtk-ui-err-badge{color:var(--st-anomaly);margin-left:6px;cursor:help;font-weight:700;}
121
+ .dtk-ui-tagchips{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px;}
122
+ .dtk-ui-tagchip{font-family:var(--mono);font-size:9.5px;color:var(--faint);
123
+ background:var(--surface-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;}
124
+ .dtk-ui-interval{font-family:var(--mono);color:var(--muted);white-space:nowrap;}
125
+ .dtk-ui-sparkcell{width:150px;}
126
+ .dtk-ui-spark-empty{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-style:italic;}
127
+ .dtk-ui-spark-loading{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-style:italic;
128
+ animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
129
+ .dtk-ui-alertscell{white-space:nowrap;}
130
+ .dtk-ui-alerts-n{font-family:var(--mono);color:var(--text);}
131
+ .dtk-ui-alerts-n.hasany{font-weight:700;color:var(--text-strong);}
132
+ .dtk-ui-alerts-n.overbudget{color:var(--st-anomaly);}
133
+ .dtk-ui-alerts-sub{font-family:var(--mono);font-size:10.5px;color:var(--faint);margin-left:5px;}
134
+ .dtk-ui-lastalert{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap;}
135
+ .dtk-ui-rate{font-family:var(--mono);color:var(--text);white-space:nowrap;}
136
+ .dtk-ui-quality{white-space:nowrap;cursor:help;}
137
+ .dtk-ui-quality-chip{font-family:var(--mono);font-size:11px;color:var(--muted);}
138
+ .dtk-ui-quality-chip b{color:var(--text);font-weight:600;}
139
+ .dtk-ui-quality.empty{color:var(--faint);}
140
+ .dtk-ui-lock{font-family:var(--mono);font-size:9px;letter-spacing:0.04em;color:var(--st-nodata);
141
+ border:1px solid var(--st-nodata);border-radius:4px;padding:1px 5px;cursor:help;}
142
+ .dtk-ui-actionscell{white-space:nowrap;text-align:right;}
143
+ .dtk-ui-actionbtn{border:1px solid var(--border);background:transparent;color:var(--muted);
144
+ font-family:var(--sans);font-size:11px;padding:4px 9px;border-radius:6px;cursor:pointer;
145
+ margin-left:5px;}
146
+ .dtk-ui-actionbtn:hover{border-color:var(--clay);color:var(--text);}
147
+
148
+ /* --- detail overlay --------------------------------------------------------- */
149
+ .dtk-ui-overlay{position:fixed;inset:0;background:rgba(10,9,7,0.72);z-index:50;display:flex;
150
+ align-items:center;justify-content:center;padding:24px;}
151
+ .dtk-ui-overlay-modal{width:100%;height:100%;max-width:1500px;background:var(--surface);
152
+ border:1px solid var(--border);border-radius:12px;display:flex;flex-direction:column;
153
+ overflow:hidden;box-shadow:0 30px 80px -20px rgba(0,0,0,0.6);}
154
+ .dtk-ui-overlay-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
155
+ padding:11px 16px;border-bottom:1px solid var(--border);background:var(--surface-2);flex:0 0 auto;}
156
+ .dtk-ui-overlay-title{font-family:var(--mono);font-size:13.5px;color:var(--text-strong);
157
+ font-weight:700;}
158
+ .dtk-ui-overlay-sub{font-family:var(--mono);font-size:11px;color:var(--faint);margin-left:8px;}
159
+ .dtk-ui-overlay-actions{display:flex;align-items:center;gap:8px;}
160
+ .dtk-ui-overlay-close{border:1px solid var(--border);background:transparent;color:var(--muted);
161
+ border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:14px;}
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;position:relative;background:var(--term-bg);}
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);}}
171
+
172
+ /* --- drawers (run panel + jobs) --------------------------------------------- */
173
+ .dtk-ui-drawer-backdrop{position:fixed;inset:0;background:rgba(10,9,7,0.5);z-index:40;display:none;}
174
+ .dtk-ui-drawer-backdrop.open{display:block;}
175
+ .dtk-ui-drawer{position:fixed;top:0;right:0;bottom:0;width:min(440px,100vw);background:var(--surface);
176
+ border-left:1px solid var(--border);z-index:41;display:none;flex-direction:column;
177
+ box-shadow:-20px 0 50px -20px rgba(0,0,0,0.6);}
178
+ .dtk-ui-drawer.open{display:flex;}
179
+ .dtk-ui-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
180
+ padding:13px 16px;border-bottom:1px solid var(--border);flex:0 0 auto;}
181
+ .dtk-ui-drawer-title{font-family:var(--mono);font-size:13px;color:var(--text-strong);
182
+ font-weight:700;text-transform:uppercase;letter-spacing:0.04em;}
183
+ .dtk-ui-drawer-close{border:1px solid var(--border);background:transparent;color:var(--muted);
184
+ border-radius:7px;width:28px;height:28px;cursor:pointer;font-size:13px;}
185
+ .dtk-ui-drawer-close:hover{border-color:var(--st-anomaly);color:var(--st-anomaly);}
186
+ .dtk-ui-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:14px 16px;display:flex;
187
+ flex-direction:column;gap:14px;}
188
+
189
+ .dtk-ui-field{display:flex;flex-direction:column;gap:5px;}
190
+ .dtk-ui-field-label{font-family:var(--mono);font-size:10.5px;color:var(--faint);
191
+ text-transform:uppercase;letter-spacing:0.05em;}
192
+ .dtk-ui-input,.dtk-ui-select{background:var(--bg);color:var(--text);border:1px solid var(--border);
193
+ border-radius:7px;padding:8px 10px;font-family:var(--mono);font-size:12.5px;width:100%;}
194
+ .dtk-ui-input:focus,.dtk-ui-select:focus{outline:none;border-color:var(--clay);}
195
+ .dtk-ui-input::placeholder{color:var(--faint);}
196
+ .dtk-ui-row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
197
+ .dtk-ui-checks{display:flex;gap:14px;flex-wrap:wrap;}
198
+ .dtk-ui-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);
199
+ cursor:pointer;}
200
+ .dtk-ui-check input{accent-color:var(--clay);cursor:pointer;}
201
+ .dtk-ui-btnrow{display:flex;gap:8px;flex-wrap:wrap;}
202
+ .dtk-ui-btn{border:1px solid var(--border);background:var(--surface-2);color:var(--text);
203
+ font-family:var(--sans);font-size:13px;font-weight:600;padding:9px 15px;border-radius:8px;
204
+ cursor:pointer;flex:1 1 auto;}
205
+ .dtk-ui-btn:hover{border-color:var(--clay);}
206
+ .dtk-ui-btn:disabled{opacity:0.45;cursor:default;}
207
+ .dtk-ui-btn.primary{background:var(--clay);border-color:var(--clay);color:#fff;}
208
+ .dtk-ui-btn.primary:hover{background:var(--clay-700);}
209
+ .dtk-ui-btn.danger{background:transparent;border-color:var(--st-anomaly);color:var(--st-anomaly);}
210
+ .dtk-ui-btn.danger:hover{background:rgba(214,50,50,0.12);}
211
+ .dtk-ui-reason{font-size:11.5px;color:var(--st-nodata);}
212
+ .dtk-ui-log{background:var(--term-bg);border:1px solid var(--term-border);border-radius:9px;
213
+ padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--term-text);
214
+ max-height:320px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;}
215
+ .dtk-ui-log-empty{color:var(--faint);font-style:italic;}
216
+ .dtk-ui-log-line.exit-ok{color:var(--st-recovery);font-weight:700;}
217
+ .dtk-ui-log-line.exit-fail{color:var(--st-anomaly);font-weight:700;}
218
+ .dtk-ui-log-line.exit-stop{color:var(--st-nodata);font-weight:700;}
219
+
220
+ /* --- jobs drawer ------------------------------------------------------------ */
221
+ .dtk-ui-joblist{display:flex;flex-direction:column;gap:7px;}
222
+ .dtk-ui-jobrow{display:flex;flex-direction:column;gap:4px;background:var(--surface-2);
223
+ border:1px solid var(--border);border-radius:8px;padding:9px 11px;cursor:pointer;}
224
+ .dtk-ui-jobrow:hover{border-color:var(--clay);}
225
+ .dtk-ui-jobrow.active{border-color:var(--clay);box-shadow:inset 0 0 0 1px var(--clay);}
226
+ .dtk-ui-jobrow-top{display:flex;align-items:center;justify-content:space-between;gap:8px;}
227
+ .dtk-ui-jobrow-status{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);
228
+ font-size:10.5px;text-transform:uppercase;letter-spacing:0.04em;}
229
+ .dtk-ui-jobrow-dot{width:7px;height:7px;border-radius:50%;flex:0 0 auto;}
230
+ .dtk-ui-jobrow-dot.pulse{animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
231
+ .dtk-ui-jobrow-label{font-family:var(--mono);font-size:12px;color:var(--text);word-break:break-word;}
232
+ .dtk-ui-jobrow-meta{font-family:var(--mono);font-size:10.5px;color:var(--faint);}
233
+ .dtk-ui-jobrow-actions{display:flex;gap:6px;margin-top:2px;}
234
+ .dtk-ui-joblink{font-family:var(--mono);font-size:11px;color:var(--clay);}
235
+
236
+ /* --- toasts ------------------------------------------------------------------ */
237
+ .dtk-toasts{position:fixed;right:18px;bottom:18px;z-index:60;display:flex;flex-direction:column;
238
+ gap:8px;max-width:360px;}
239
+ .dtk-toast{background:var(--surface);border:1px solid var(--border);border-radius:9px;
240
+ padding:11px 14px;font-family:var(--sans);font-size:13px;color:var(--text);
241
+ box-shadow:0 12px 30px -12px rgba(0,0,0,0.5);animation:dtk-ui-toast-in 0.18s ease-out;}
242
+ .dtk-toast-error{border-color:var(--st-anomaly);}
243
+ .dtk-toast-info{border-color:var(--clay);}
244
+ .dtk-toast-out{opacity:0;transform:translateY(6px);transition:opacity 0.2s,transform 0.2s;}
245
+ @keyframes dtk-ui-toast-in{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
246
+
247
+ /* --- responsive ------------------------------------------------------------- */
248
+ @media (max-width:860px){
249
+ .dtk-ui-drawer{width:100vw;}
250
+ .dtk-ui-row2{grid-template-columns:1fr;}
251
+ .dtk-ui-table-wrap{overflow-x:auto;}
252
+ }
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"}
254
+ alert rule: ${n}
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};})();
@@ -27,6 +27,7 @@ from detectkit.autotune.labels import IncidentLabels, newest_labels_file, parse_
27
27
  from detectkit.config.metric_config import AlertConfig, MetricConfig
28
28
  from detectkit.config.project_config import ProjectConfig
29
29
  from detectkit.database.internal_tables import InternalTablesManager
30
+ from detectkit.detectors.factory import DetectorFactory
30
31
  from detectkit.reporting.builder import _ms, _num_or_none, record_from_row, replay_alert_events
31
32
  from detectkit.tuning.payload import DEFAULT_FALSE_ALERT_BUDGET
32
33
  from detectkit.utils.datetime_utils import now_utc_naive
@@ -95,6 +96,60 @@ def _alert_rule_summary(alerting: list[AlertConfig] | None) -> dict[str, Any] |
95
96
  }
96
97
 
97
98
 
99
+ def _configured_detector_ids(config: MetricConfig) -> list[str]:
100
+ """The detector ids the metric's CURRENT config would run under.
101
+
102
+ Mirrors the detect step's id derivation (``get_algorithm_params`` +
103
+ seasonality → ``DetectorFactory.create_from_config`` → ``get_detector_id``).
104
+ Every retune/autotune changes a detector's id, and the superseded ids'
105
+ rows stay in ``_dtk_detections`` forever — so an unfiltered window read
106
+ returns one row-set per historical config generation: N× the volume, and
107
+ replayed quorums that mix live and dead configs (inflating alert counts
108
+ a real pipeline run would never produce). The overview answers "how does
109
+ the metric behave under its current config", so it reads only these ids.
110
+
111
+ Returns ``[]`` when no id can be derived (no detectors configured, or a
112
+ config the factory rejects) — the caller then falls back to unfiltered.
113
+ """
114
+ ids: list[str] = []
115
+ for detector_config in config.detectors:
116
+ try:
117
+ params = detector_config.get_algorithm_params()
118
+ seasonality = detector_config.get_seasonality_components()
119
+ if seasonality is not None:
120
+ params["seasonality_components"] = seasonality
121
+ detector = DetectorFactory.create_from_config(
122
+ {"type": detector_config.type, "params": params}
123
+ )
124
+ ids.append(detector.get_detector_id())
125
+ except Exception: # noqa: BLE001 — fall back to unfiltered on any bad config
126
+ return []
127
+ return ids
128
+
129
+
130
+ def _load_window_detections(
131
+ internal: InternalTablesManager,
132
+ metric_name: str,
133
+ detector_ids: list[str],
134
+ start: datetime,
135
+ to_exclusive: datetime,
136
+ ) -> list[dict[str, Any]]:
137
+ """Window detections for the given detector ids (all ids when empty).
138
+
139
+ Per-id queries push the filter into SQL instead of transferring every
140
+ historical generation's rows; the merged result is re-sorted to the
141
+ global ``(timestamp, detector_id)`` order ``load_detections`` guarantees,
142
+ which the replay walk expects.
143
+ """
144
+ if not detector_ids:
145
+ return internal.load_detections(metric_name, None, start, to_exclusive)
146
+ rows: list[dict[str, Any]] = []
147
+ for detector_id in detector_ids:
148
+ rows.extend(internal.load_detections(metric_name, detector_id, start, to_exclusive))
149
+ rows.sort(key=lambda r: (r["timestamp"], r["detector_id"]))
150
+ return rows
151
+
152
+
98
153
  def _resolve_metric_window(
99
154
  internal: InternalTablesManager,
100
155
  metric_name: str,
@@ -329,7 +384,9 @@ def _fill_stats(
329
384
  row["points"] = n_points
330
385
  row["first_point_in_window"] = _ms(ts_arr[0]) if n_points else None
331
386
 
332
- det_rows = internal.load_detections(name, None, start, to_exclusive)
387
+ det_rows = _load_window_detections(
388
+ internal, name, _configured_detector_ids(config), start, to_exclusive
389
+ )
333
390
  det_rows = [r for r in det_rows if _ms(r["timestamp"]) <= end_ms]
334
391
 
335
392
  anomalous_timestamps: set[int] = set()
@@ -386,6 +443,48 @@ def _fill_stats(
386
443
  row["spark_anoms"] = _downsample_evenly(sorted(anomalous_timestamps), _MAX_SPARK_ANOMS)
387
444
 
388
445
 
446
+ def resolve_project_budget(project_config: ProjectConfig) -> float:
447
+ """The project-level ``false_alert_budget`` fallback (metric overrides it)."""
448
+ if project_config.false_alert_budget is not None:
449
+ return project_config.false_alert_budget
450
+ return DEFAULT_FALSE_ALERT_BUDGET
451
+
452
+
453
+ def build_metric_row(
454
+ *,
455
+ project_config: ProjectConfig,
456
+ project_root: Path,
457
+ metric_path: Path,
458
+ config: MetricConfig,
459
+ internal: InternalTablesManager,
460
+ window_preset: str,
461
+ now: datetime | None = None,
462
+ ) -> dict[str, Any]:
463
+ """One metric's overview row — the unit ``GET /api/stats/<name>`` serves.
464
+
465
+ The cockpit fetches rows one metric at a time (bounded, seconds each)
466
+ instead of one monolithic all-metrics payload: on a production-sized
467
+ project the combined read (full detections of every metric × the window,
468
+ serialized on the single DB connection) takes minutes — long enough for
469
+ the browser to abort the request as failed while the page shows an
470
+ endless spinner. Raises ``ValueError`` for an unknown *window_preset*.
471
+ """
472
+ if window_preset not in ALL_WINDOW_PRESETS:
473
+ allowed = ", ".join(sorted(ALL_WINDOW_PRESETS))
474
+ raise ValueError(f"Unknown window preset {window_preset!r}. Choose one of: {allowed}.")
475
+ return _build_metric_row(
476
+ project_root=project_root,
477
+ metrics_dir=project_root / "metrics",
478
+ metric_path=metric_path,
479
+ config=config,
480
+ internal=internal,
481
+ window_preset=window_preset,
482
+ now=now if now is not None else now_utc_naive(),
483
+ project_name=project_config.name,
484
+ project_budget=resolve_project_budget(project_config),
485
+ )
486
+
487
+
389
488
  def _build_metric_row(
390
489
  *,
391
490
  project_root: Path,
@@ -444,11 +543,7 @@ def build_overview_payload(
444
543
 
445
544
  now_dt = now if now is not None else now_utc_naive()
446
545
  metrics_dir = project_root / "metrics"
447
- project_budget = (
448
- project_config.false_alert_budget
449
- if project_config.false_alert_budget is not None
450
- else DEFAULT_FALSE_ALERT_BUDGET
451
- )
546
+ project_budget = resolve_project_budget(project_config)
452
547
 
453
548
  rows = [
454
549
  _build_metric_row(