detectkit 0.37.0__tar.gz → 0.38.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {detectkit-0.37.0/detectkit.egg-info → detectkit-0.38.0}/PKG-INFO +1 -1
  2. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/labels.py +48 -1
  4. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/cli.md +30 -23
  5. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/tune.py +6 -0
  6. detectkit-0.38.0/detectkit/tuning/assets/tune.js +147 -0
  7. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/payload.py +4 -0
  8. {detectkit-0.37.0 → detectkit-0.38.0/detectkit.egg-info}/PKG-INFO +1 -1
  9. detectkit-0.37.0/detectkit/tuning/assets/tune.js +0 -132
  10. {detectkit-0.37.0 → detectkit-0.38.0}/LICENSE +0 -0
  11. {detectkit-0.37.0 → detectkit-0.38.0}/MANIFEST.in +0 -0
  12. {detectkit-0.37.0 → detectkit-0.38.0}/README.md +0 -0
  13. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/__init__.py +0 -0
  14. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/__init__.py +0 -0
  15. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/base.py +0 -0
  16. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/branding.py +0 -0
  17. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/email.py +0 -0
  18. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/factory.py +0 -0
  19. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/mattermost.py +0 -0
  20. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/slack.py +0 -0
  21. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/telegram.py +0 -0
  22. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/webhook.py +0 -0
  23. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  24. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  25. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  26. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  27. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  28. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  29. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  30. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  31. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  32. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/__init__.py +0 -0
  33. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/_base.py +0 -0
  34. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/_types.py +0 -0
  35. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/autotuner.py +0 -0
  36. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/config_emitter.py +0 -0
  37. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/crossval.py +0 -0
  38. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/detector_select.py +0 -0
  39. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/distribution.py +0 -0
  40. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/grid_search.py +0 -0
  41. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/html_labeler.py +0 -0
  42. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/label_server.py +0 -0
  43. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/result.py +0 -0
  44. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/scoring.py +0 -0
  45. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/seasonality_search.py +0 -0
  46. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/settings.py +0 -0
  47. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/window_select.py +0 -0
  48. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/__init__.py +0 -0
  49. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/_output.py +0 -0
  50. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  51. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  52. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  53. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  54. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  55. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  56. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  57. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  58. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  59. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  60. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  61. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/__init__.py +0 -0
  62. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/autotune.py +0 -0
  63. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/clean.py +0 -0
  64. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/init.py +0 -0
  65. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/init_claude.py +0 -0
  66. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/run.py +0 -0
  67. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/test_alert.py +0 -0
  68. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/unlock.py +0 -0
  69. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/main.py +0 -0
  70. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/__init__.py +0 -0
  71. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/metric_config.py +0 -0
  72. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/profile.py +0 -0
  73. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/project_config.py +0 -0
  74. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/validator.py +0 -0
  75. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/__init__.py +0 -0
  76. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/interval.py +0 -0
  77. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/models.py +0 -0
  78. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/__init__.py +0 -0
  79. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/_sql_manager.py +0 -0
  80. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/clickhouse_manager.py +0 -0
  81. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/__init__.py +0 -0
  82. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  83. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  84. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_base.py +0 -0
  85. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  86. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_detections.py +0 -0
  87. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  88. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  89. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_schema.py +0 -0
  90. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  91. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/manager.py +0 -0
  92. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/manager.py +0 -0
  93. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/mysql_manager.py +0 -0
  94. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/postgres_manager.py +0 -0
  95. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/tables.py +0 -0
  96. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/__init__.py +0 -0
  97. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/base.py +0 -0
  98. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/factory.py +0 -0
  99. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/seasonality.py +0 -0
  100. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/__init__.py +0 -0
  101. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  102. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/iqr.py +0 -0
  103. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/mad.py +0 -0
  104. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  105. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/zscore.py +0 -0
  106. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/__init__.py +0 -0
  107. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/metric_loader.py +0 -0
  108. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/query_template.py +0 -0
  109. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/__init__.py +0 -0
  110. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/error_dispatch.py +0 -0
  111. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  112. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  113. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  114. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  115. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  116. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  117. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  118. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/__init__.py +0 -0
  119. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/assets/report.js +0 -0
  120. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/builder.py +0 -0
  121. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/html_report.py +0 -0
  122. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/__init__.py +0 -0
  123. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/config_writer.py +0 -0
  124. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/html.py +0 -0
  125. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/server.py +0 -0
  126. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/__init__.py +0 -0
  127. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/datetime_utils.py +0 -0
  128. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/env_interpolation.py +0 -0
  129. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/json_utils.py +0 -0
  130. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/stats.py +0 -0
  131. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/SOURCES.txt +0 -0
  132. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/dependency_links.txt +0 -0
  133. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/entry_points.txt +0 -0
  134. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/requires.txt +0 -0
  135. {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/top_level.txt +0 -0
  136. {detectkit-0.37.0 → detectkit-0.38.0}/pyproject.toml +0 -0
  137. {detectkit-0.37.0 → detectkit-0.38.0}/requirements.txt +0 -0
  138. {detectkit-0.37.0 → detectkit-0.38.0}/setup.cfg +0 -0
  139. {detectkit-0.37.0 → detectkit-0.38.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.37.0
3
+ Version: 0.38.0
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.37.0"
7
+ __version__ = "0.38.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -67,6 +67,11 @@ class IncidentLabels:
67
67
  # metadata: it records the regime scope the user reasoned about (auditable in
68
68
  # the saved file, restored on reopen); it does NOT affect ground truth.
69
69
  capture_windows: list[tuple[datetime, datetime]] = field(default_factory=list)
70
+ # Optional per-alert review verdicts from `dtk tune` (streak span + 'valid' /
71
+ # 'false'). Pure metadata: it re-seeds the green/slate markers on reopen (a
72
+ # confirmed alert is also written as a normal incident, which IS ground truth).
73
+ # autotune ignores this block entirely.
74
+ alert_reviews: list[tuple[datetime, datetime, str]] = field(default_factory=list)
70
75
 
71
76
  def is_empty(self) -> bool:
72
77
  return not self.intervals and not self.points
@@ -158,6 +163,7 @@ def parse_incident_labels(
158
163
  return IncidentLabels([], [])
159
164
 
160
165
  raw_windows: list = []
166
+ raw_reviews: list = []
161
167
  if isinstance(raw, list):
162
168
  entries = raw
163
169
  tz: ZoneInfo | None = None
@@ -173,6 +179,9 @@ def parse_incident_labels(
173
179
  raw_windows = raw.get("capture_windows") or []
174
180
  if not isinstance(raw_windows, list):
175
181
  raise ValueError("'capture_windows' must be a list")
182
+ raw_reviews = raw.get("alert_reviews") or []
183
+ if not isinstance(raw_reviews, list):
184
+ raise ValueError("'alert_reviews' must be a list")
176
185
  else:
177
186
  raise ValueError("Labels must be a mapping with 'incidents' or a list of incidents")
178
187
 
@@ -205,7 +214,22 @@ def parse_incident_labels(
205
214
  raise ValueError(f"Capture window start {ws} is after end {we}")
206
215
  capture_windows.append((ws, we))
207
216
 
208
- return IncidentLabels(intervals=intervals, points=points, capture_windows=capture_windows)
217
+ alert_reviews: list[tuple[datetime, datetime, str]] = []
218
+ for rv in raw_reviews:
219
+ if not isinstance(rv, dict) or "start" not in rv or "end" not in rv:
220
+ raise ValueError("Each alert_reviews entry needs 'start' and 'end'")
221
+ rs, re_ = _parse_dt(rv["start"], tz), _parse_dt(rv["end"], tz)
222
+ if rs > re_:
223
+ raise ValueError(f"Alert review start {rs} is after end {re_}")
224
+ verdict = "false" if str(rv.get("verdict", "valid")) == "false" else "valid"
225
+ alert_reviews.append((rs, re_, verdict))
226
+
227
+ return IncidentLabels(
228
+ intervals=intervals,
229
+ points=points,
230
+ capture_windows=capture_windows,
231
+ alert_reviews=alert_reviews,
232
+ )
209
233
 
210
234
 
211
235
  def parse_labels_file(
@@ -282,6 +306,29 @@ def load_capture_windows(
282
306
  return capture_windows_to_display(labels)
283
307
 
284
308
 
309
+ def alert_reviews_to_display(labels: IncidentLabels) -> list[dict[str, str]]:
310
+ """Render parsed per-alert review verdicts as display dicts (naive-UTC strings)."""
311
+ return [
312
+ {
313
+ "start": start.strftime(_DISPLAY_FMT),
314
+ "end": end.strftime(_DISPLAY_FMT),
315
+ "verdict": verdict,
316
+ }
317
+ for start, end, verdict in labels.alert_reviews
318
+ ]
319
+
320
+
321
+ def load_alert_reviews(
322
+ path: str | Path,
323
+ *,
324
+ interval_seconds: int,
325
+ metric_name: str | None = None,
326
+ ) -> list[dict[str, str]]:
327
+ """Load a labels file and render its per-alert review verdicts as display dicts."""
328
+ labels = parse_labels_file(path, interval_seconds=interval_seconds, metric_name=metric_name)
329
+ return alert_reviews_to_display(labels)
330
+
331
+
285
332
  # Versioned labels store helpers (shared by the autotune labeler server and the
286
333
  # `dtk tune` labeler so both name + discover files the same way).
287
334
  _LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
@@ -80,38 +80,45 @@ view of the metric's **real** persisted series and lets you turn the detector's
80
80
  knobs (type — including **Manual bounds** with lower/upper sliders — threshold,
81
81
  window, recency weighting + half-life, detrend, smoothing, **seasonality groups**,
82
82
  **direction** (both/up/down), alert `consecutive_anomalies`) while the confidence
83
- band and flagged anomalies **recompute live**. The chart is **zoomable** (scroll/drag +
84
- a navigator strip) and a **"Points shown"** slider trims the active sample to speed
85
- up recompute on a long metric. Clicking **Apply** writes the chosen
83
+ band and flagged anomalies **recompute live**. The whole screen is a **chart-first
84
+ cockpit**: ONE chart (the windshield) fills the view, every knob lives in a
85
+ **collapsible dock under it**, the metrics ride beneath the chart, and the chart is
86
+ **zoomable** (scroll/drag + navigator strip) with a **"Points shown"** trim slider.
87
+ Clicking **Apply** writes the chosen
86
88
  config back into the metric YAML **in place** (autotune, by contrast, writes a new
87
89
  `__tuned_<id>.yml` and never edits the original). Reads the metric's loaded
88
90
  datapoints (run `dtk run --steps load` first if empty); the selector must resolve
89
91
  to a single metric.
90
92
 
91
- **Mark incidents + see alert quality live.** A second, **synced** chart beneath
92
- the detector view **mirrors the detector's anomaly dots** and lets you **mark the
93
- real incidents** (drag to create a span, drag its edges/middle to adjust, ✕ or
94
- Delete to remove), **Lasso anomalies** (loop around a cloud of anomaly dots each
95
- run of consecutive anomalies, small gaps bridged up to `consecutive_anomalies`,
96
- becomes one incident span), or **Threshold capture**
97
- every contiguous span past a horizontal line in one shot (set the line by click or
98
- value, above/below, optional gap-bridging, optionally limited to a painted time
99
- window the same tool as the autotune labeler; the window is saved as
100
- `capture_windows` and restored on reopen; each captured span is widened to a full
101
- interval so the alert lands inside). As you tune, a metrics
102
- bar shows two operator numbers: **incident catch rate (recall)** how many marked
103
- incidents your config catches (an incident is caught when an alert's anomaly
104
- **streak overlaps** it, not just the fire instant) and **false-alert rate**
105
- what share of alerts fall outside any real incident ("≈1 in N false", keeping a
106
- decimal so a mostly-false rate doesn't round to a misleading "1 in 1"); only
107
- incidents within the loaded window are scored. **Save incidents** writes a
93
+ **Modes + alert review + live quality.** A **mode switch** picks which layers lead
94
+ and which interactions are armed on the one chart: **Tune** (band leads; incidents
95
+ recede to read-only context; hover a point for its window), **Review** (the fired
96
+ alerts lead, band ghosts click an alert marker to cycle its verdict un-reviewed →
97
+ **valid** (green) **false alarm** (slate); **Confirm all unreviewed valid** does
98
+ the lot), and **Label** (band hides; **mark the real incidents** by drag, **Lasso
99
+ anomalies** loop a cloud of anomaly dots, each consecutive run, gaps bridged up to
100
+ `consecutive_anomalies`, becomes one span or **Threshold capture** every span past
101
+ a horizontal line, each widened to a full interval so the alert lands inside; the
102
+ painted window saves as `capture_windows`). A confirmed (valid) alert is the user
103
+ asserting a real incident happened there it folds in as a virtual incident (counts
104
+ toward recall + correct) so a clean metric whose alerts are all good is validated in
105
+ a few clicks **without hand-drawing spans**, and is written as an incident on Save.
106
+ As you tune, a metrics bar shows **incident catch rate (recall)** how many
107
+ ground-truth incidents (marked + validated) your config catches (an incident is
108
+ caught when an alert's anomaly **streak overlaps** it, not just the fire instant)
109
+ **false-alert rate** (what share of alerts fall outside any incident and aren't
110
+ confirmed valid; "≈1 in N false", a decimal so a mostly-false rate doesn't round to
111
+ a misleading "1 in 1") — and **reviewed N/M**; only incidents within the loaded
112
+ window are scored. **Save incidents** writes a
108
113
  versioned `incidents/<metric>/*.yml` — the same store `dtk autotune` reads, so the
109
114
  same labels feed the next supervised tune (it seeds from the newest file on open,
110
115
  **anchoring the budget-sized loaded window on the seeded incidents** — ending just
111
116
  past the latest one rather than at the last datapoint — so they render/count
112
- without an old outlier pulling the whole history in). Saving incidents does not end
113
- the session; only **Apply** does. A
114
- **y = 0 line** toggle (shared with `dtk run --report`) shows the metric relative to zero.
117
+ without an old outlier pulling the whole history in; per-alert verdicts persist as
118
+ an `alert_reviews:` metadata block and re-seed on reopen, re-bound to the moved
119
+ alerts by streak overlap). Saving incidents does not end the session; only **Apply**
120
+ does. A **y = 0 line** toggle (shared with `dtk run --report`) shows the metric
121
+ relative to zero.
115
122
 
116
123
  Safe write-back: the config is validated before anything is written, the previous
117
124
  YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  import click
17
17
 
18
18
  from detectkit.autotune.labels import (
19
+ load_alert_reviews,
19
20
  load_capture_windows,
20
21
  load_incidents_for_display,
21
22
  newest_labels_file,
@@ -70,6 +71,7 @@ def run_tune(
70
71
  incidents_dir = project_root / "incidents" / name
71
72
  preload_incidents: list[dict[str, str]] = []
72
73
  preload_capture: list[dict[str, str]] = []
74
+ preload_reviews: list[dict[str, str]] = []
73
75
  newest = newest_labels_file(incidents_dir)
74
76
  if newest is not None:
75
77
  try:
@@ -79,6 +81,9 @@ def run_tune(
79
81
  preload_capture = load_capture_windows(
80
82
  newest, interval_seconds=interval_seconds, metric_name=name
81
83
  )
84
+ preload_reviews = load_alert_reviews(
85
+ newest, interval_seconds=interval_seconds, metric_name=name
86
+ )
82
87
  click.echo(
83
88
  f" Seeded {len(preload_incidents)} incident(s) from "
84
89
  f"{newest.relative_to(project_root)}"
@@ -99,6 +104,7 @@ def run_tune(
99
104
  project_name=project_name,
100
105
  incidents=preload_incidents,
101
106
  capture_windows=preload_capture,
107
+ alert_reviews=preload_reviews,
102
108
  )
103
109
  n_points = len(payload["points"])
104
110
  if n_points == 0:
@@ -0,0 +1,147 @@
1
+ "use strict";(()=>{var Wn={"--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 U(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim()||Wn[n]||"#888"}function Rn(n){let c=n.replace("#","").trim();c.length===3&&(c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]);let b=parseInt(c,16);return c.length!==6||Number.isNaN(b)?[209,91,54]:[b>>16&255,b>>8&255,b&255]}function H(n,c){let[b,e,w]=Rn(n);return`rgba(${b},${e},${w},${c})`}function Sn(n){let c=Math.max(1,window.devicePixelRatio||1),b=n.clientWidth||n.offsetWidth||0,e=n.clientHeight||n.offsetHeight||0;return n.width=Math.round(b*c),n.height=Math.round(e*c),c}var gn=Number.isFinite;function vn(n,c,b,e,w,N,G,h,y,z,I,B){let we=c.length,se=Math.max(1,Math.round(G)),ae=w-e||1,Ye=0;for(let ee=0;ee<we;ee++){let a=b[ee];!gn(a)||c[ee]<e||c[ee]>w||Ye++}if(n.strokeStyle=z,n.lineWidth=I*B,n.lineJoin="round",n.beginPath(),Ye<=se){let ee=!1;for(let a=0;a<we;a++){let g=b[a],D=c[a];if(!gn(g)||D<e||D>w){ee=!1;continue}let E=h(D),ne=y(g);ee?n.lineTo(E,ne):(n.moveTo(E,ne),ee=!0)}}else{let ee=new Array(se).fill(null),a=new Array(se).fill(null);for(let D=0;D<we;D++){let E=b[D],ne=c[D];if(!gn(E)||ne<e||ne>w)continue;let M=Math.floor((ne-e)/ae*(se-1));M=M<0?0:M>se-1?se-1:M,(ee[M]===null||E<ee[M])&&(ee[M]=E),(a[M]===null||E>a[M])&&(a[M]=E)}let g=!1;for(let D=0;D<se;D++){if(a[D]===null){g=!1;continue}let E=N+D,ne=y(a[D]),M=y(ee[D]);g?n.lineTo(E,ne):(n.moveTo(E,ne),g=!0),n.lineTo(E,M)}}n.stroke()}function Cn(n){let c=Math.abs(n);return c>=1e3?n.toFixed(0):c>=10?n.toFixed(1):c>=1?n.toFixed(2):n.toFixed(3)}function Tn(n,c){let b=new Date(n).toISOString();return c<2*864e5?b.slice(5,16).replace("T"," "):b.slice(5,10)}function An(n,c,b){return{left:c.l*b,top:c.t*b,right:n.width-c.r*b,bottom:n.height-c.b*b}}function Dn(n,c,b,e,w,N,G){let h=An(c,b,e),y=Math.max(h.left,Math.min(w(N),h.right));y<=h.left+.5||(n.save(),n.fillStyle="rgba(17,15,13,0.42)",n.fillRect(h.left,h.top,y-h.left,h.bottom-h.top),n.strokeStyle=H(U("--faint"),.7),n.lineWidth=1*e,n.setLineDash([4*e,4*e]),n.beginPath(),n.moveTo(y,h.top),n.lineTo(y,h.bottom),n.stroke(),n.setLineDash([]),n.fillStyle=H(U("--faint"),.95),n.font=`${10*e}px ui-monospace, monospace`,n.textAlign="left",n.textBaseline="top",n.fillText(G,y+6*e,h.top+5*e),n.restore())}function Ln(n,c,b,e,w,N,G){let h=An(c,b,e),y=5*e;n.save();for(let z=0;z<N.length;z++){let I=N[z],B=w(I.t);if(B<h.left-1||B>h.right+1)continue;let we=G(I.kind);n.strokeStyle=H(we,.45),n.lineWidth=1*e,n.beginPath(),n.moveTo(B,h.top),n.lineTo(B,h.bottom),n.stroke(),n.fillStyle=we,n.beginPath(),n.moveTo(B-y,h.top),n.lineTo(B+y,h.top),n.lineTo(B,h.top+y*1.4),n.closePath(),n.fill()}n.restore()}var In={mad:1,zscore:2,iqr:4};function zn(n,c){let b=n.seasonalityData;if(!b||b.length===0)return 0;let e=0;for(let w of c){let N=new Set;for(let G of b)N.add(w.map(h=>{var y;return String((y=G==null?void 0:G[h])!=null?y:"")}).join("|"));e=Math.max(e,N.size)}return e}function un(n,c){let b=n.timestamps.length;if(c.type==="manual_bounds")return Math.min(c.inputType!=="values"?1:0,b);let e=Math.max(c.minSamples,In[c.type]);if(c.smoothing==="sma"&&(e=Math.max(e,c.smoothingWindow-1)),c.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/c.smoothingAlpha))),c.inputType!=="values"&&(e=Math.max(e,1)),c.seasonalityComponents!==null&&c.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0){let N=zn(n,c.seasonalityComponents);if(N>0){let G=c.minSamplesPerGroup*N;c.windowSize>=G&&(e=Math.max(e,G))}}return Math.min(e,b)}var A={l:52,r:14,t:14,b:26},xn=46,Hn=10,Bn=13,ye=Number.isFinite,bt=1e3,gt=60*bt,vt=60*gt,he=24*vt,yn=[bt,2*bt,5*bt,10*bt,15*bt,30*bt,gt,2*gt,5*gt,10*gt,15*gt,30*gt,vt,2*vt,3*vt,6*vt,12*vt,he,2*he,3*he,5*he,7*he,14*he];function Pn(n,c,b){let e=Math.max(c-n,1);if(e>b*14*he){let h=[1,2,3,6,12,24,36,60,120,240],y=h[h.length-1];for(let ae of h)if(e/(ae*30.44*he)<=b){y=ae;break}let z=new Date(n),I=z.getUTCFullYear(),B=z.getUTCMonth();if(y>=12){let ae=y/12;I=Math.floor(I/ae)*ae,B=0}else B=Math.floor(B/y)*y;let we=[],se=Date.UTC(I,B,1);for(;se<=c;)se>=n&&we.push(se),B+=y,I+=Math.floor(B/12),B%=12,se=Date.UTC(I,B,1);return{ticks:we,step:y*30*he}}let w=yn[yn.length-1];for(let h of yn)if(e/h<=b){w=h;break}let N;if(w%he===0){let h=new Date(n);N=Date.UTC(h.getUTCFullYear(),h.getUTCMonth(),h.getUTCDate());let y=w/he,z=Math.round(N/he);N+=(y-z%y)%y*he}else N=Math.ceil(n/w)*w;let G=[];for(let h=N;h<=c;h+=w)h>=n&&G.push(h);return{ticks:G,step:w}}function wn(n,c){let b=new Date(n).toISOString();return c>=320*he?b.slice(0,4):c>=26*he?b.slice(0,7):c>=he?b.slice(5,10):c>=vt?b.slice(5,16).replace("T"," "):b.slice(5,19).replace("T"," ")}function Nn(n,c={}){var _t;let b=n.getContext("2d");if(!b)throw new Error("chart: 2D context unavailable");let e=b,w=!!c.navigable,N=w&&c.showNavigator!==!1,G=c.yFit==="data",h=!!c.labeling,y=(_t=c.mode)!=null?_t:"tune",z=()=>h?y:"tune",I=()=>z()!=="label",B=()=>z()==="review"?.3:1,we=()=>z()==="tune"?1:z()==="review"?.3:.85,se=()=>h&&z()==="label",ae=()=>z()==="label"?1:.55,Ye=()=>z()==="tune",ee=()=>h&&(z()==="tune"||z()==="review"),a=1,g=null,D=-1,E=0,ne=!!c.showZeroLine,M=[],te=null,Be=-1,V=null,xt=!1,be=!1,yt="above",Rt=0,Re=null,Oe=null,ke=null,de=null,Pe=null,Te=!1,Y=null,O=0,j=1,Ae=0,Fe=1,oe=0,F=1,wt="";function It(){a=Sn(n)}let De=()=>N?(Hn+xn)*a:0,J=()=>n.width-(A.l+A.r)*a,Ee=()=>n.height-(A.t+A.b)*a-De(),Ie=()=>w?oe:O,Ke=()=>w?F:j,Ne=()=>Ke()-Ie()||1,Ve=()=>j-O||1,L=o=>A.l*a+(o-Ie())/Ne()*J(),ce=o=>n.height-A.b*a-De()-(o-Ae)/(Fe-Ae||1)*Ee(),Xe=()=>n.height-xn*a,ue=()=>(xn-Bn)*a,st=()=>n.width-(A.l+A.r)*a,ge=o=>A.l*a+(o-O)/Ve()*st(),Mn=o=>Xe()+ue()-(o-Ae)/(Fe-Ae||1)*ue(),zt=(o,r,l)=>{let i=new Array(o.length).fill(!1);if(o.length===0)return i;let s=e.measureText(wn(o[0],l)).width+16*a,u=-1/0;for(let m=0;m<o.length;m++){let v=r(o[m]);v-u>=s&&(i[m]=!0,u=v)}return i},Z=(o,r,l)=>Math.max(r,Math.min(l,o)),qe=()=>{let o=g==null?void 0:g.series,r=o?o.timestamps.length:0,l=r>1?Ve()/(r-1):1e3;return Math.max(l*8,1e3)};function ve(o,r){var s;let l=r-o,i=qe();if(l<i){let u=(o+r)/2;o=u-i/2,r=u+i/2,l=i}l>=Ve()&&(o=O,r=j),o<O&&(r+=O-o,o=O),r>j&&(o-=r-j,r=j),oe=Z(o,O,j),F=Z(r,O,j),xt||(s=c.onViewChange)==null||s.call(c,oe,F),be&&Q(),R()}function mn(o,r){w&&(xt=!0,ve(o,r),xt=!1)}let Je=o=>{let r=(o-A.l*a)/(J()||1);return Ie()+Z(r,0,1)*Ne()},Ze=o=>{let r=(o-A.l*a)/(st()||1);return O+Z(r,0,1)*Ve()},ze=o=>{let r=n.height-A.b*a-De(),l=Z((r-o)/(Ee()||1),0,1);return Ae+l*(Fe-Ae)};function at(o){let r=g==null?void 0:g.series;if(!r||r.timestamps.length===0)return-1;let l=(o-A.l*a)/(J()||1),i=Ie()+l*Ne(),s=r.timestamps,u=0,m=s.length-1;for(;u<m;){let v=u+m>>1;s[v]<i?u=v+1:m=v}return u>0&&i-s[u-1]<s[u]-i&&(u-=1),u}function lt(o,r){let l=o.timestamps;if(O=l[0],j=l[l.length-1],w){let m=`${l.length}:${O}:${j}`;m!==wt?(wt=m,oe=O,F=j):(oe=Z(oe,O,j),F=Z(Math.max(F,oe+qe()),O,j))}let i=1/0,s=-1/0;for(let m of o.values)ye(m)&&(m<i&&(i=m),m>s&&(s=m));if(!G)for(let m of r)m.scored&&(ye(m.lower)&&m.lower<i&&(i=m.lower),ye(m.upper)&&m.upper>s&&(s=m.upper));(!ye(i)||!ye(s))&&(i=0,s=1),ne&&(i>0&&(i=0),s<0&&(s=0)),s<=i&&(s=i+1);let u=(s-i)*.06;Ae=i-u,Fe=s+u}function kt(o,r,l){let i=g.series.timestamps;vn(e,i,o,Ie(),Ke(),A.l*a,J(),L,ce,r,l,a)}function le(o,r){let l=[],i=-1;for(let s=Math.max(0,r);s<o.length;s++){let u=o[s];u.scored&&ye(u.lower)&&ye(u.upper)?i===-1&&(i=s):i!==-1&&(l.push([i,s-1]),i=-1)}return i!==-1&&l.push([i,o.length-1]),l}function Ht(){if(E=0,!g||n.width===0||n.height===0)return;let{series:o,scored:r,params:l,alerts:i}=g;if(o.timestamps.length===0){e.fillStyle=U("--term-bg"),e.fillRect(0,0,n.width,n.height);return}let s=U("--clay"),u=U("--st-anomaly"),m=U("--faint"),v=U("--muted");e.fillStyle=U("--term-bg"),e.fillRect(0,0,n.width,n.height),e.font=`${11*a}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let x=0;x<=4;x++){let $=Ae+(Fe-Ae)*x/4,W=ce($);e.strokeStyle=H(m,.1),e.lineWidth=1*a,e.beginPath(),e.moveTo(A.l*a,W),e.lineTo(n.width-A.r*a,W),e.stroke(),e.fillStyle=v,e.textAlign="right",e.fillText(Cn($),(A.l-8)*a,W)}if(ne&&Ae<=0&&Fe>=0){let x=ce(0);e.strokeStyle=H(v,.6),e.lineWidth=1.25*a,e.beginPath(),e.moveTo(A.l*a,x),e.lineTo(n.width-A.r*a,x),e.stroke(),e.fillStyle=v,e.textAlign="right",e.fillText("0",(A.l-8)*a,x)}e.textBaseline="top";let P=n.height-A.b*a-De();if(w){let x=Pn(Ie(),Ke(),7),$=zt(x.ticks,L,x.step);for(let W=0;W<x.ticks.length;W++){let re=x.ticks[W],Se=L(re);e.strokeStyle=H(m,.1),e.lineWidth=1*a,e.beginPath(),e.moveTo(Se,A.t*a),e.lineTo(Se,P),e.stroke(),$[W]&&(e.fillStyle=v,e.textAlign=Se<(A.l+24)*a?"left":Se>n.width-(A.r+24)*a?"right":"center",e.fillText(wn(re,x.step),Se,P+7*a))}}else{let x=Ne();for(let $=0;$<=5;$++){let W=O+x*$/5,re=L(W);e.fillStyle=v,e.textAlign=$===0?"left":$===5?"right":"center",e.fillText(Tn(W,x),re,P+7*a)}}e.save(),e.beginPath(),e.rect(A.l*a,A.t*a,J(),Ee()),e.clip(),ut(),Ut();let S=r.length,T=Math.min(un(o,l),S),q=T<S?o.timestamps[T]:void 0,K=B(),fe=I()?le(r,T):[];e.fillStyle=H(s,.13*K);for(let[x,$]of fe){e.beginPath(),e.moveTo(L(r[x].timestamp),ce(r[x].upper));for(let W=x+1;W<=$;W++)e.lineTo(L(r[W].timestamp),ce(r[W].upper));for(let W=$;W>=x;W--)e.lineTo(L(r[W].timestamp),ce(r[W].lower));e.closePath(),e.fill()}e.strokeStyle=H(s,.4*K),e.lineWidth=1*a;for(let[x,$]of fe)for(let W of["upper","lower"]){e.beginPath();for(let re=x;re<=$;re++){let Se=L(r[re].timestamp),t=ce(r[re][W]);re===x?e.moveTo(Se,t):e.lineTo(Se,t)}e.stroke()}e.strokeStyle=H(m,.55*K),e.lineWidth=1*a,e.setLineDash([3*a,3*a]);for(let[x,$]of fe){e.beginPath();for(let W=x;W<=$;W++){let re=r[W].center;if(!ye(re))continue;let Se=L(r[W].timestamp),t=ce(re);W===x?e.moveTo(Se,t):e.lineTo(Se,t)}e.stroke()}if(e.setLineDash([]),l.smoothing!=="none"&&(!h||z()==="tune")){kt(o.values,H(s,.28),1.25);let x=r.map($=>$.processedValue);kt(x,s,1.6)}else kt(o.values,s,1.5);let X=we();for(let x=T;x<S;x++){let $=r[x];if(!$.scored||!ye($.value))continue;let W=L($.timestamp),re=ce($.value);$.isAnomaly?(e.fillStyle=H(u,.18*X),e.beginPath(),e.arc(W,re,6*a,0,Math.PI*2),e.fill(),e.fillStyle=H(u,X),e.beginPath(),e.arc(W,re,3*a,0,Math.PI*2),e.fill()):o.truthAnomaly[x]&&(e.strokeStyle=H(v,.7),e.lineWidth=1.25*a,e.beginPath(),e.arc(W,re,3.5*a,0,Math.PI*2),e.stroke())}q!==void 0&&(!h||z()==="tune")&&Dn(e,n,A,a,L,q,"detection at full power \u2192"),D>=0&&D<r.length&&Ye()&&Bt(D,l.windowSize,r,o,m),i&&i.length&&Ln(e,n,A,a,L,i,x=>x==="anomaly"?U("--st-anomaly"):x==="anomaly-validated"?U("--st-recovery"):x==="anomaly-false"?U("--st-error"):x==="recovery"?U("--st-recovery"):U("--st-nodata")),tt(),jt(r,T,S),e.restore(),N&&Qe(o,i,s,m,v)}function Qe(o,r,l,i,s){let u=Xe(),m=u+ue(),v=A.l*a,P=n.width-A.r*a,S=Pn(O,j,5);e.save(),e.beginPath(),e.rect(v,u,st(),ue()),e.clip(),e.strokeStyle=H(i,.1),e.lineWidth=1*a;for(let X of S.ticks){let x=ge(X);e.beginPath(),e.moveTo(x,u),e.lineTo(x,m),e.stroke()}vn(e,o.timestamps,o.values,O,j,v,st(),ge,Mn,H(l,.7),1.1,a),e.restore();let T=ge(oe),q=ge(F);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(v,u,T-v,ue()),e.fillRect(q,u,P-q,ue()),M.length){let X=U("--st-anomaly");e.fillStyle=H(X,.28);for(let x of M){let $=ge(x.start),W=Math.max(ge(x.end)-$,2*a);e.fillRect($,u,W,ue())}}if(r&&r.length){let X=U("--st-anomaly");e.fillStyle=H(X,.85);for(let x of r){let $=ge(x.t);e.fillRect($-1*a,u,2*a,ue())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(T,u,q-T,ue()),e.strokeStyle=l,e.lineWidth=1.5*a,e.strokeRect(T,u+1,q-T,ue()-2),e.fillStyle=l;let K=u+ue()/2-8*a;e.fillRect(T-2*a,K,4*a,16*a),e.fillRect(q-2*a,K,4*a,16*a),e.font=`${10*a}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=s;let fe=zt(S.ticks,ge,S.step);for(let X=0;X<S.ticks.length;X++){let x=S.ticks[X],$=ge(x);e.strokeStyle=H(i,.25),e.lineWidth=1*a,e.beginPath(),e.moveTo($,m),e.lineTo($,m+3*a),e.stroke(),fe[X]&&(e.textAlign=$<(A.l+26)*a?"left":$>n.width-(A.r+26)*a?"right":"center",e.fillText(wn(x,S.step),$,m+5*a))}}function Bt(o,r,l,i,s){let u=i.timestamps,m=Math.max(0,o-r),v=o-1,P=A.t*a,S=Ee();if(v>=m){let K=i.intervalSeconds*1e3/Ne()*J()*.5,fe=L(u[m])-K,X=L(u[v])+K;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(fe,P,X-fe,S),e.strokeStyle=H(s,.5),e.lineWidth=1*a,e.beginPath(),e.moveTo(fe,P),e.lineTo(fe,P+S),e.moveTo(X,P),e.lineTo(X,P+S),e.stroke()}let T=L(u[o]);e.strokeStyle=H(s,.85),e.lineWidth=1*a,e.setLineDash([2*a,2*a]),e.beginPath(),e.moveTo(T,P),e.lineTo(T,P+S),e.stroke(),e.setLineDash([]);let q=l[o];if(q.scored&&ye(q.lower)&&ye(q.upper)){let K=5*a;e.strokeStyle=H(U("--clay"),.85),e.lineWidth=1.5*a;for(let fe of[q.lower,q.upper]){let X=ce(fe);e.beginPath(),e.moveTo(T-K,X),e.lineTo(T+K,X),e.stroke()}}if(ye(q.value)){let K=ce(q.value);e.fillStyle=U("--term-bg"),e.beginPath(),e.arc(T,K,4*a,0,Math.PI*2),e.fill(),e.strokeStyle=q.isAnomaly?U("--st-anomaly"):U("--clay"),e.lineWidth=2*a,e.beginPath(),e.arc(T,K,4*a,0,Math.PI*2),e.stroke()}}function dt(o,r,l,i,s){e.beginPath(),e.moveTo(o+s,r),e.arcTo(o+l,r,o+l,r+i,s),e.arcTo(o+l,r+i,o,r+i,s),e.arcTo(o,r+i,o,r,s),e.arcTo(o,r,o+l,r,s),e.closePath()}function ct(o,r){let l=U("--st-anomaly"),i=14*a,s=3*a,u=o-i-s,m=A.t*a+s;e.fillStyle=r?H(l,.95):"rgba(27,25,22,0.82)",e.strokeStyle=H(l,.9),e.lineWidth=1*a,dt(u,m,i,i,3*a),e.fill(),e.stroke(),e.strokeStyle=r?"#fff":l,e.lineWidth=1.5*a;let v=4*a;e.beginPath(),e.moveTo(u+v,m+v),e.lineTo(u+i-v,m+i-v),e.moveTo(u+i-v,m+v),e.lineTo(u+v,m+i-v),e.stroke()}let xe=()=>Re!=null?Re:Oe,et=()=>{let o=Pe||ke;return o?[Math.min(o.a,o.b),Math.max(o.a,o.b)]:[Ie(),Ke()]};function _e(){let o=[],r=xe();if(r==null||!g)return o;let l=g.series.timestamps,i=g.series.values,[s,u]=et(),m=-1,v=-1,P=0;for(let S=0;S<l.length;S++){if(l[S]<s||l[S]>u)continue;let T=i[S];ye(T)&&(yt==="above"?T>r:T<r)?(m===-1&&(m=l[S]),v=l[S],P=0):m!==-1&&(P++,P>Rt&&(o.push([m,v]),m=-1,P=0))}return m!==-1&&o.push([m,v]),o}function Q(){if(!c.onThresholdChange)return;let[o,r]=et(),l=Pe||ke;c.onThresholdChange({value:xe(),locked:Re!=null,runs:_e().length,window:l?{start:Math.min(l.a,l.b),end:Math.max(l.a,l.b)}:null,committed:ke!=null,windowMs:r-o})}function Ot(o,r){let l=null;for(let i=M.length-1;i>=0;i--){let s=M[i];o<=s.end&&r>=s.start&&(l===null?(s.start=Math.min(s.start,o),s.end=Math.max(s.end,r),l=s):(l.start=Math.min(l.start,s.start),l.end=Math.max(l.end,s.end),te===s&&(te=l),M.splice(i,1)))}l===null&&M.push({start:o,end:r,label:""})}function ut(){if(!be||xe()==null)return;let r=A.t*a,l=Ee(),i=U("--st-nodata");for(let[s,u]of _e()){let m=L(s),v=Math.max(L(u)-m,2*a);e.fillStyle=H(i,.22),e.fillRect(m,r,v,l),e.strokeStyle=H(i,.6),e.lineWidth=1*a,e.strokeRect(m,r,v,l)}}function tt(){if(!be)return;let o=A.t*a,r=n.height-A.b*a-De(),l=A.l*a,i=n.width-A.r*a,s=U("--st-nodata"),[u,m]=et(),v=!!(Pe||ke),P=Z(L(u),l,i),S=Z(L(m),l,i);v&&(e.fillStyle=H(U("--ink"),.5),e.fillRect(l,o,Math.max(0,P-l),Ee()),e.fillRect(S,o,Math.max(0,i-S),Ee()),e.strokeStyle=H(s,.7),e.lineWidth=1*a,e.setLineDash([3*a,3*a]),e.beginPath(),e.moveTo(P,o),e.lineTo(P,r),e.moveTo(S,o),e.lineTo(S,r),e.stroke(),e.setLineDash([]));let T=xe();if(T!=null&&T>=Ae&&T<=Fe){let q=ce(T);e.strokeStyle=s,e.lineWidth=1.5*a,e.setLineDash([6*a,4*a]),e.beginPath(),e.moveTo(P,q),e.lineTo(S,q),e.stroke(),e.setLineDash([])}}let Mt=()=>g?g.series.intervalSeconds*1e3:1e3;function Ft(o,r,l){let i=!1;for(let s=0,u=l.length-1;s<l.length;u=s++){let m=l[s].x,v=l[s].y,P=l[u].x,S=l[u].y;v>r!=S>r&&o<(P-m)*(r-v)/(S-v||1e-9)+m&&(i=!i)}return i}function je(o,r,l){let i=[];if(!Y||Y.length<3)return i;for(let s=Math.max(0,r);s<l;s++){let u=o[s];!u.scored||!u.isAnomaly||!ye(u.value)||Ft(L(u.timestamp),ce(u.value),Y)&&i.push(s)}return i}function Vt(o,r){var S;if(!r.length)return[];let l=Mt(),s=(Math.max(1,(S=g==null?void 0:g.params.consecutiveAnomalies)!=null?S:1)+1)*l,u=l/2,m=[],v=r[0],P=r[0];for(let T=1;T<r.length;T++){let q=r[T];o[q].timestamp-o[P].timestamp>s&&(m.push([o[v].timestamp-u,o[P].timestamp+u]),v=q),P=q}return m.push([o[v].timestamp-u,o[P].timestamp+u]),m}function qt(o,r,l){if(!c.onLassoChange)return;let i=Te&&Y?je(o,r,l):[];c.onLassoChange({active:!!Y,anomalies:i.length,incidents:Vt(o,i).length})}function nt(){if(!g||!Y)return;let{scored:o}=g,r=o.length,l=Math.min(un(g.series,g.params),r),i=Vt(o,je(o,l,r));for(let[s,u]of i)Ot(s,u);i.length&&Ue()}function jt(o,r,l){if(!Te||!Y||Y.length<2)return;let i=U("--clay"),s=U("--st-anomaly");e.beginPath(),e.moveTo(Y[0].x,Y[0].y);for(let u=1;u<Y.length;u++)e.lineTo(Y[u].x,Y[u].y);e.closePath(),e.fillStyle=H(i,.08),e.fill(),e.strokeStyle=H(i,.9),e.lineWidth=1.5*a,e.setLineDash([5*a,4*a]),e.stroke(),e.setLineDash([]);for(let u of je(o,r,l)){let m=L(o[u].timestamp),v=ce(o[u].value);e.strokeStyle=s,e.lineWidth=2*a,e.beginPath(),e.arc(m,v,7*a,0,Math.PI*2),e.stroke()}}function Ut(){let o=A.t*a,r=Ee(),l=U("--st-anomaly"),i=A.l*a,s=n.width-A.r*a,u=se(),m=ae();for(let v=0;v<M.length;v++){let P=M[v],S=L(P.start),T=L(P.end);if(T<i-1||S>s+1)continue;let q=Math.max(T-S,2*a),K=u&&P===te;e.fillStyle=H(l,(K?.3:.16)*m),e.fillRect(S,o,q,r),e.strokeStyle=H(l,(K?.95:.5)*m),e.lineWidth=(K?2:1)*a,e.strokeRect(S,o,q,r),u&&(e.fillStyle=H(l,.95),e.fillRect(S-1.5*a,o,3*a,r),e.fillRect(T-1.5*a,o,3*a,r),(T-S>=22*a||K)&&ct(T,K||v===Be))}if(u&&V&&V.mode==="new"){let v=L(V.a),P=L(V.b);e.fillStyle=H(U("--st-nodata"),.28),e.fillRect(Math.min(v,P),o,Math.abs(P-v),r)}}function St(o,r){if(!g||!g.alerts||!g.alerts.length||r>(A.t+22)*a)return-1;let l=-1,i=9*a;for(let s=0;s<g.alerts.length;s++){let u=Math.abs(o-L(g.alerts[s].t));u<=i&&(i=u,l=s)}return l}function Ct(o){var i;if(!g||!g.alerts)return;let r=g.alerts[o].kind,l=r==="anomaly"?"valid":r==="anomaly-validated"?"false":"unreviewed";(i=c.onAlertReviewChange)==null||i.call(c,g.alerts[o].t,l)}function Ue(){var o;(o=c.onIncidentsChange)==null||o.call(c,M)}function Tt(o){let r=M.indexOf(o);r<0||(M.splice(r,1),te===o&&(te=null),Be=-1,Ue(),R())}function ot(o,r){let l=A.t*a,i=6*a;for(let s=0;s<M.length;s++){let u=L(M[s].end);if(u-L(M[s].start)>=22*a||M[s]===te){let m=14*a,v=3*a,P=u-m-v,S=l+v;if(o>=P&&o<=P+m&&r>=S&&r<=S+m)return{i:s,edge:"del"}}}for(let s=0;s<M.length;s++){let u=L(M[s].start),m=L(M[s].end);if(Math.abs(o-u)<=i)return{i:s,edge:"a"};if(Math.abs(o-m)<=i)return{i:s,edge:"b"}}for(let s=0;s<M.length;s++){let u=L(M[s].start),m=L(M[s].end);if(o>u+i&&o<m-i)return{i:s,edge:"move"}}return null}let mt=()=>{let o=g==null?void 0:g.series,r=o?o.timestamps.length:0;return r>1?Math.max(Ve()/(r-1),1):1e3};function R(){E===0&&(E=requestAnimationFrame(Ht))}function $e(){if(!g)return;let o=g.scored.length,r=Math.min(un(g.series,g.params),o);qt(g.scored,r,o)}function Me(){var l;if(!c.onHover||!g)return;if(D<0){c.onHover(null);return}let o=(l=g.scored[D])!=null?l:null,r={index:D,point:o,windowStart:Math.max(0,D-g.params.windowSize),windowEnd:D-1};c.onHover(r)}let Le=null,me=null,rt=o=>{let r=n.getBoundingClientRect();return{x:(o.clientX-r.left)*a,y:(o.clientY-r.top)*a}},fn=o=>N&&o>=Xe(),At=o=>{let r=ge(oe),l=ge(F),i=8*a;return Math.abs(o-r)<=i?"l":Math.abs(o-l)<=i?"r":o>r&&o<l?"move":"out"};function Dt(o){if(!g)return;let{x:r,y:l}=rt(o);if(Le||me||V)return;if(fn(l)){D!==-1&&(D=-1,Me(),R());let u=At(r);n.style.cursor=u==="l"||u==="r"?"ew-resize":u==="move"?"grab":"pointer";return}let i=z();if(h&&i==="label"&&Te){D!==-1&&(D=-1,Me()),n.style.cursor="crosshair";return}if(h&&i==="label"&&be){D!==-1&&(D=-1,Me()),Re==null&&!de&&(Oe=ze(l),Q(),R()),n.style.cursor="crosshair";return}if(h&&i==="label"){D!==-1&&(D=-1,Me());let u=ot(r,l),m=u&&u.edge==="del"?u.i:-1;m!==Be&&(Be=m,R()),n.style.cursor=u?u.edge==="del"?"pointer":u.edge==="move"?"grab":"ew-resize":"crosshair";return}if(h&&i==="review"){D!==-1&&(D=-1,Me()),n.style.cursor=St(r,l)>=0?"pointer":"grab";return}w&&(n.style.cursor=h&&St(r,l)>=0?"pointer":"grab");let s=at(r);s!==D&&(D=s,Me(),R())}function pe(){D!==-1&&(D=-1,Me(),R())}function ft(o){if(!w||!g)return;let{x:r,y:l}=rt(o);if(N&&l>=Xe()){let i=At(r);if(i==="l")me={type:"l",grab:0,vMin:oe,vMax:F};else if(i==="r")me={type:"r",grab:0,vMin:oe,vMax:F};else if(i==="move")me={type:"move",grab:Ze(r),vMin:oe,vMax:F};else{let s=Ze(r),u=Ne();ve(s-u/2,s+u/2),me={type:"move",grab:s,vMin:oe,vMax:F}}n.style.cursor="grabbing",o.preventDefault();return}if(l>A.t*a&&l<n.height-A.b*a-De()){let i=z();if(ee()){let s=St(r,l);if(s>=0){Ct(s),o.preventDefault();return}}if(h&&i==="label"&&Te){Y=[{x:r,y:l}],$e(),n.style.cursor="crosshair",R(),o.preventDefault();return}if(h&&i==="label"&&be){de={x:r,ts:Je(r)},Re==null&&(Oe=ze(l)),Q(),R(),o.preventDefault();return}if(h&&i==="label"){let s=Je(r),u=ot(r,l);if(u&&u.edge==="del"){Tt(M[u.i]),o.preventDefault();return}if(u&&u.edge==="move"){let m=M[u.i];te=m,V={mode:"move",iv:m,grab:s,a0:m.start,b0:m.end,moved:!1}}else if(u){let m=M[u.i];te=m,V={mode:"edge",iv:m,edge:u.edge,moved:!1}}else te=null,V={mode:"new",a:s,b:s,moved:!1};n.style.cursor="crosshair",R(),o.preventDefault();return}Le={x:r,vMin:oe,vMax:F},n.style.cursor="grabbing",o.preventDefault()}}function Lt(o){if(!g)return;let{x:r,y:l}=rt(o);if(Te&&Y){Y.push({x:r,y:l}),$e(),R();return}if(be&&de){Math.abs(r-de.x)>6*a?Pe={a:de.ts,b:Je(r)}:(Pe=null,Re==null&&(Oe=ze(l))),Q(),R();return}if(V){let i=Je(r);if(V.mode==="new")V.b=i,Math.abs(L(V.b)-L(V.a))>4*a&&(V.moved=!0);else if(V.mode==="edge"){let s=V.iv,u=mt();V.edge==="a"?s.start=Z(Math.min(i,s.end-u),O,j):s.end=Z(Math.max(i,s.start+u),O,j),V.moved=!0}else{let s=V.iv,u=V.a0+(i-V.grab),m=V.b0+(i-V.grab);u<O&&(m+=O-u,u=O),m>j&&(u-=m-j,m=j),s.start=Z(u,O,j),s.end=Z(m,O,j),V.moved=!0}R();return}if(Le){let i=(r-Le.x)*(Le.vMax-Le.vMin)/(J()||1);ve(Le.vMin-i,Le.vMax-i)}else if(me){let i=Ze(r);me.type==="l"?ve(Math.min(i,F-qe()),F):me.type==="r"?ve(oe,Math.max(i,oe+qe())):ve(me.vMin+(i-me.grab),me.vMax+(i-me.grab))}}function Pt(o){if(Te&&Y){nt(),Y=null,$e(),R();return}if(be&&de){let{x:r,y:l}=rt(o);if(Math.abs(r-de.x)>6*a){let i=de.ts,s=Je(r);ke={a:Math.min(i,s),b:Math.max(i,s)}}else Re=ze(l);de=null,Pe=null,Q(),R();return}if(V){let r=V;if(r.mode==="new")if(r.moved){let l=Z(Math.min(r.a,r.b),O,j),i=Z(Math.max(r.a,r.b),O,j),s={start:l,end:i,label:""};M.push(s),te=s}else te=null;else if(r.iv.start>r.iv.end){let l=r.iv.start;r.iv.start=r.iv.end,r.iv.end=l}V=null,n.style.cursor="crosshair",r.moved&&Ue(),R();return}(Le||me)&&(Le=null,me=null,n.style.cursor="grab")}function it(o){let r=o.target,l=!!r&&(r.tagName==="INPUT"||r.tagName==="TEXTAREA"||r.isContentEditable);if(Te){o.key==="Escape"&&Y&&(Y=null,$e(),R());return}be||l||!te||(o.key==="Delete"||o.key==="Backspace"?(o.preventDefault(),Tt(te)):o.key==="Escape"&&(te=null,R()))}function Gt(o){if(!w||!g)return;if(Te&&Y){o.preventDefault();return}o.preventDefault();let{x:r,y:l}=rt(o),i=Z(Ne()*Math.pow(1.0015,o.deltaY),qe(),Ve());if(l>=Xe()){let s=Ze(r);ve(s-i/2,s+i/2)}else{let s=Je(r),u=(s-oe)/(Ne()||1);ve(s-u*i,s-u*i+i)}}function Yt(){w&&ve(O,j)}n.addEventListener("mousemove",Dt),n.addEventListener("mouseleave",pe),w&&(n.addEventListener("mousedown",ft),n.addEventListener("wheel",Gt,{passive:!1}),n.addEventListener("dblclick",Yt),window.addEventListener("mousemove",Lt),window.addEventListener("mouseup",Pt)),h&&(window.addEventListener("keydown",it),n.style.cursor=y==="tune"?"grab":"crosshair");function pn(o){g=o,!h&&o.incidents&&(M=o.incidents),lt(o.series,o.scored),D>=o.series.timestamps.length&&(D=-1),R()}function hn(o){ne!==o&&(ne=o,g&&lt(g.series,g.scored),R())}function an(o){M=o,te=null,Be=-1,R()}function Kt(o){h&&(be=o,o?(te=null,Te=!1,Y=null,$e()):(Oe=null,de=null,Pe=null),n.style.cursor="crosshair",Q(),R())}function pt(o){h&&(Te=o,o?(be=!1,de=null,Pe=null,Oe=null,te=null,Q()):Y=null,n.style.cursor="crosshair",$e(),R())}function ln(o){!h||y===o||(y=o,o!=="label"&&(be=!1,de=null,Pe=null,Oe=null,Te=!1,Y=null,te=null,Be=-1,Q(),$e()),D=-1,n.style.cursor=o==="tune"?"grab":"crosshair",g&&lt(g.series,g.scored),R())}function Xt(o){yt=o,Q(),R()}function Jt(o){Rt=Math.max(0,Math.floor(o)||0),Q(),R()}function Zt(o){Re=o!=null&&ye(o)?o:null,Q(),R()}function Qt(){let o=_e(),r=Mt()/2;for(let[l,i]of o)Ot(l-r,i+r);return o.length&&Ue(),Q(),R(),o.length}function dn(){ke=null,Pe=null,Q(),R()}function bn(){return ke?{start:Math.min(ke.a,ke.b),end:Math.max(ke.a,ke.b)}:null}function cn(o){ke=o?{a:o.start,b:o.end}:null,Q(),R()}function Nt(){It(),g&&(lt(g.series,g.scored),R())}function Et(){n.removeEventListener("mousemove",Dt),n.removeEventListener("mouseleave",pe),w&&(n.removeEventListener("mousedown",ft),n.removeEventListener("wheel",Gt),n.removeEventListener("dblclick",Yt),window.removeEventListener("mousemove",Lt),window.removeEventListener("mouseup",Pt)),h&&window.removeEventListener("keydown",it),E!==0&&cancelAnimationFrame(E),E=0,g=null}return It(),{render:pn,resize:Nt,setMode:ln,setZeroLine:hn,setViewWindow:mn,setIncidents:an,setThresholdMode:Kt,setThresholdDirection:Xt,setThresholdGap:Jt,setThresholdValue:Zt,applyThreshold:Qt,setLassoMode:pt,clearCaptureWindow:dn,getCaptureWindow:bn,setCaptureWindow:cn,destroy:Et}}var On={mad:3,zscore:3,iqr:1.5},Fn={mad:10,zscore:3,iqr:4},Vn="dtk-tune";function p(n,c,b){let e=document.createElement(n);return c&&(e.className=c),b!=null&&(e.textContent=b),e}function sn(n,c){let b=p("label","dtk-ctl-label",n);if(c){b.title=c;let e=p("span","dtk-ctl-info","\u24D8");e.title=c,b.appendChild(document.createTextNode(" ")),b.appendChild(e)}return b}function nn(n,c,b,e,w){let N=p("div","dtk-ctl");N.appendChild(sn(n,w));let G=p("div","dtk-seg"),h=b,y=[],z=()=>{y.forEach(I=>I.classList.toggle("on",I.dataset.v===h))};return c.forEach(I=>{let B=p("button","dtk-seg-btn",I.label);B.type="button",B.dataset.v=I.value,B.onclick=()=>{h=I.value,z(),e(h)},y.push(B),G.appendChild(B)}),z(),N.appendChild(G),{row:N,get:()=>h,set:I=>{h=I,z()}}}function Wt(n,c,b){var z;let e=p("div","dtk-ctl"),w=p("div","dtk-ctl-head"),N=sn(n,c.hint),G=p("span","dtk-ctl-val"),h=(z=c.fmt)!=null?z:(I=>String(I));w.appendChild(N),w.appendChild(G),e.appendChild(w);let y=p("input","dtk-range");return y.type="range",y.min=String(c.min),y.max=String(c.max),y.step=String(c.step),y.value=String(c.value),G.textContent=h(c.value),y.oninput=()=>{let I=Number(y.value);G.textContent=h(I),b(I)},e.appendChild(y),{row:e,get:()=>Number(y.value),setMax:I=>{y.max=String(I),Number(y.value)>I&&(y.value=String(I),G.textContent=h(I))}}}function _n(n){if(n.type==="manual_bounds"){let b={};return n.lowerBound!=null&&(b.lower_bound=n.lowerBound),n.upperBound!=null&&(b.upper_bound=n.upperBound),n.inputType!=="values"&&(b.input_type=n.inputType),b}let c={threshold:n.threshold,window_size:n.windowSize};return n.windowWeights!=="none"&&(c.window_weights=n.windowWeights,n.windowWeights==="exponential"&&n.halfLife!=null&&(c.half_life=n.halfLife)),n.detrend!=="none"&&(c.detrend=n.detrend),n.smoothing!=="none"&&(c.smoothing=n.smoothing),n.inputType!=="values"&&(c.input_type=n.inputType),n.seasonalityComponents&&n.seasonalityComponents.length&&(c.seasonality_components=n.seasonalityComponents,c.min_samples_per_group=n.minSamplesPerGroup),c}function qn(n,c){let b=_n(n),e=[`type: ${n.type}`];for(let[w,N]of Object.entries(b))e.push(`${w}=${typeof N=="object"?JSON.stringify(N):N}`);return n.direction&&n.direction!=="any"&&e.push(`direction=${n.direction}`),e.push(`consecutive_anomalies=${c}`),e.join(" \xB7 ")}function jn(n,c){var W,re,Se;Gn(),c.classList.add(Vn),c.innerHTML="";let b=p("div","dtk-tune-root");c.appendChild(b);let e=n.points.length,w={timestamps:n.points.map(t=>t.t),values:n.points.map(t=>t.v==null?NaN:t.v),intervalSeconds:n.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:n.seasonality_columns.length?n.seasonality:void 0,seasonalityColumns:n.seasonality_columns.length?n.seasonality_columns:void 0},N=w,G=t=>{let d=Math.max(0,e-t);return d<=0?w:{timestamps:w.timestamps.slice(d),values:w.values.slice(d),intervalSeconds:w.intervalSeconds,truthAnomaly:w.truthAnomaly.slice(d),seasonalityData:w.seasonalityData?w.seasonalityData.slice(d):void 0,seasonalityColumns:w.seasonalityColumns}},h=t=>Date.parse(t.replace(" ","T")+"Z"),y=(n.incidents||[]).map(t=>({start:h(t.start),end:h(t.end),label:t.label||""})).filter(t=>Number.isFinite(t.start)&&Number.isFinite(t.end)).map(t=>({start:Math.min(t.start,t.end),end:Math.max(t.start,t.end),label:t.label})),z=(()=>{let t=(n.capture_windows||[])[0];if(!t)return null;let d=h(t.start),f=h(t.end);return!Number.isFinite(d)||!Number.isFinite(f)?null:{start:Math.min(d,f),end:Math.max(d,f)}})(),I=[],B=[],we=[],se=[],ae=(n.alert_reviews||[]).map(t=>({start:h(t.start),end:h(t.end),verdict:t.verdict==="false"?"false":"valid"})).filter(t=>Number.isFinite(t.start)&&Number.isFinite(t.end)),Ye=()=>n.interval_seconds*1e3/2,ee=(t,d)=>{let f=Ye();for(let k of ae)if(d>=k.start-f&&t<=k.end+f)return k.verdict;return null},a=(t,d,f)=>{let k=Ye();for(let C=ae.length-1;C>=0;C--){let _=ae[C];d>=_.start-k&&t<=_.end+k&&ae.splice(C,1)}f&&ae.push({start:t,end:d,verdict:f})},g=()=>B.filter(([t,d])=>ee(t,d)==="valid").map(([t,d])=>({start:t,end:d,label:"alert \u2713"})),D=()=>I.map((t,d)=>{var C;let f=(C=B[d])!=null?C:[t,t],k=ee(f[0],f[1]);return{t,kind:k==="valid"?"anomaly-validated":k==="false"?"anomaly-false":"anomaly"}}),E=n.detector,ne=n.consecutive_anomalies,M=w.values.filter(t=>Number.isFinite(t)).sort((t,d)=>t-d),te=M.length?M[0]:0,Be=M.length?M[M.length-1]:1,V=t=>M.length?M[Math.min(M.length-1,Math.max(0,Math.round(t*(M.length-1))))]:0,xt=Math.max((Be-te)*.05,1e-9),be=te-xt,yt=Be+xt,Rt=Math.max((yt-be)/400,1e-9),Re=E.lowerBound!=null?E.lowerBound:V(.05),Oe=E.upperBound!=null?E.upperBound:V(.95),ke=(W=E.seasonalityComponents)!=null?W:[],de=new Map;ke.forEach((t,d)=>t.forEach(f=>de.set(f,d+1)));let Pe=()=>{let t=0;de.forEach(f=>{f>t&&(t=f)});let d=[];for(let f=1;f<=t;f++){let k=n.seasonality_columns.filter(C=>de.get(C)===f);k.length&&d.push(k)}return d.length?d:null},Te=t=>{if(!t||!n.seasonality.length)return 0;let d=0;for(let f of t){let k=new Set;for(let C of n.seasonality)k.add(f.map(_=>{var ie;return String((ie=C==null?void 0:C[_])!=null?ie:"")}).join("|"));d=Math.max(d,k.size)}return d},Y=()=>{var t;return{type:ft.get(),threshold:it.get(),windowSize:Kt.get(),minSamples:E.minSamples,inputType:E.inputType,smoothing:Zt.get(),smoothingAlpha:E.smoothingAlpha,smoothingWindow:E.smoothingWindow,windowWeights:pt.get(),halfLife:pt.get()==="exponential"?ln.get():null,detrend:Jt.get(),seasonalityComponents:Pe(),minSamplesPerGroup:(t=Fn[ft.get()])!=null?t:E.minSamplesPerGroup,consecutiveAnomalies:ne,direction:dn.get(),lowerBound:Lt.get(),upperBound:Pt.get()}},O=p("div","dtk-tune-header"),j=p("div","dtk-tune-titlerow");j.appendChild(p("h1","dtk-tune-title",n.metric));let Ae=p("span","dtk-tune-badge",n.save_url?"manual tuning":"preview");j.appendChild(Ae),O.appendChild(j);let Fe=n.project?`${n.project} \xB7 `:"";O.appendChild(p("div","dtk-tune-sub",`${Fe}${e} points \xB7 ${Un(n.interval_seconds)} grid`)),n.description&&O.appendChild(p("div","dtk-tune-desc",n.description)),b.appendChild(O);let oe=p("div","dtk-tune-metrics"),F=p("div","dtk-tune-main");b.appendChild(F);let wt=p("div","dtk-tune-dock"),It=p("div","dtk-tune-dockhead"),De=p("button","dtk-dock-toggle","\u2699 Controls");De.type="button",De.title="Show or hide the control dock \u2014 collapse it to give the chart the whole screen.",It.appendChild(De);let J=p("div","dtk-tune-controls");wt.appendChild(It),wt.appendChild(J),b.appendChild(wt);let Ee=!0,Ie=t=>{Ee=t,J.style.display=t?"":"none",De.classList.toggle("on",t),De.textContent=t?"\u2699 Controls \u25BE":"\u2699 Controls \u25B8"},Ke=p("div","dtk-tune-trim"),Ne=p("div","dtk-tune-trim-head");Ne.appendChild(sn("Points shown","Trim the active sample to the most-recent N points. Fewer points recompute faster (cost grows with points \xD7 window) and make a shorter period easier to read \u2014 handy once you can see a smaller window/period is enough."));let Ve=p("span","dtk-tune-trim-val");Ne.appendChild(Ve),Ke.appendChild(Ne);let L=p("input","dtk-range");L.type="range",L.min=String(Math.min(e,200)),L.max=String(e),L.step=String(Math.max(1,Math.round(e/200))),L.value=String(e),Ke.appendChild(L),F.appendChild(Ke);let ce=p("div","dtk-tune-chart"),Xe=p("canvas");ce.appendChild(Xe);let ue=p("div","dtk-tune-spin");ue.appendChild(p("span","dtk-spin-ring")),ue.appendChild(p("span","dtk-spin-txt","computing\u2026")),ce.appendChild(ue),F.appendChild(ce);let st=p("div","dtk-tune-modes"),ge={};[{v:"tune",label:"Tune",hint:"Steer the band \u2014 the confidence corridor leads; incidents recede to read-only context. Hover a point for its window."},{v:"review",label:"Review alerts",hint:"Confirm the fired alerts \u2014 click a marker to cycle un-reviewed \u2192 valid (green) \u2192 false (slate). The band ghosts so the alerts lead."},{v:"label",label:"Label incidents",hint:"Mark real incidents \u2014 drag a span, lasso the anomaly cloud, or threshold-capture. The band hides so incidents lead."}].forEach(t=>{let d=p("button","dtk-mode-btn",t.label);d.type="button",d.title=t.hint,d.dataset.v=t.v,d.onclick=()=>St(t.v),ge[t.v]=d,st.appendChild(d)}),F.appendChild(st),F.appendChild(oe),De.onclick=()=>Ie(!Ee),Ie(!0);let zt=p("div","dtk-tune-legend"),Z=(t,d,f)=>{let k=p("span","dtk-leg-item");k.title=f,k.appendChild(p("span",`dtk-leg-sw ${t}`)),k.appendChild(p("span","dtk-leg-txt",d)),zt.appendChild(k)};Z("line","metric","The metric value over time."),Z("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),Z("center","band center","The expected value at the middle of the band."),Z("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),Z("alert","alert","A fired alert, not yet reviewed \u2014 enough consecutive anomalies to meet the rule."),Z("alert-ok","valid alert","An alert you confirmed is real (click a marker in Review mode). Counts toward recall."),Z("alert-no","false alarm","An alert you marked a false positive. Stays in the false-alert rate."),F.appendChild(zt);let qe=p("div","dtk-tune-readout");F.appendChild(qe);let ve=p("div","dtk-tune-warn");ve.style.display="none",F.appendChild(ve);let mn=t=>{let d=t.seasonalityComponents,f=Te(d),k=t.minSamplesPerGroup*f;d&&f>0&&t.windowSize<k?(ve.textContent=`\u26A0 Seasonality inactive at this window: ${t.windowSize} < ${k} (min_samples_per_group ${t.minSamplesPerGroup} \xD7 ${f} key${f===1?"":"s"}). Each point keeps only ~${Math.floor(t.windowSize/f)} same-key point(s), so the band falls back to global statistics (seasonality has no effect). Raise the window to \u2265 ${k}.`,ve.style.display=""):ve.style.display="none"},Je=(t,d)=>{let f=Ye(),k=N.timestamps,C=(k.length?k[0]:0)-f,_=(k.length?k[k.length-1]:0)+f,ie=[...d,...g()].filter(We=>We.end>=C&&We.start<=_),ht=(We,Ge)=>We[1]>=Ge.start-f&&We[0]<=Ge.end+f,$t=0,en=0,Ce=0;for(let We of t){let Ge=ee(We[0],We[1]);Ge&&Ce++,Ge==="valid"&&en++,(Ge==="valid"||Ge!=="false"&&ie.some($n=>ht(We,$n)))&&$t++}let He=0;for(let We of ie)t.some(Ge=>ht(Ge,We))&&He++;let tn=t.length;return{realIncidents:ie.length,caught:He,recall:ie.length?He/ie.length:NaN,totalAlerts:tn,correctAlerts:$t,falseAlerts:tn-$t,fdr:tn?(tn-$t)/tn:NaN,validated:en,reviewed:Ce}},Ze=t=>Number.isFinite(t)?`${Math.round(t*100)}%`:"\u2014",ze=(t,d,f,k)=>`<span class="dtk-m-chip"><span class="dtk-m-dot" style="background:${k}"></span><span class="dtk-m-v">${t}</span><span class="dtk-m-l">${d}</span>`+(f?`<span class="dtk-m-sub">${f}</span>`:"")+"</span>";function at(){let t=Je(B,y),d=t.realIncidents>0,f;if(!d)f="mark incidents to measure";else if(t.totalAlerts===0)f="no alerts";else if(t.falseAlerts===0)f=`${Ze(t.fdr)} \xB7 all correct`;else{let C=t.totalAlerts/t.falseAlerts,_=C>=9.5?String(Math.round(C)):String(Math.round(C*10)/10);f=`${Ze(t.fdr)} \xB7 \u22481 in ${_} false`}let k=t.totalAlerts===0?"no alerts":`${t.validated} valid${t.totalAlerts>t.reviewed?` \xB7 ${t.totalAlerts-t.reviewed} left`:" \xB7 all reviewed"}`;oe.innerHTML=ze(String(t.realIncidents),"real incidents","","var(--c)")+ze(d?`${t.caught}/${t.realIncidents}`:"\u2014","caught",d?`recall ${Ze(t.recall)}`:"mark or confirm","var(--green)")+ze(String(t.totalAlerts),"alerts","","var(--c)")+ze(d?String(t.falseAlerts):"\u2014","false alerts",f,"var(--anom)")+ze(`${t.reviewed}/${t.totalAlerts}`,"reviewed",k,"var(--green)")}let lt=()=>{},kt=()=>{},le=Nn(Xe,{navigable:!0,labeling:!0,mode:"tune",yFit:"data",onHover:t=>{if(!t||!t.point||!t.point.scored){qe.textContent="";return}let d=t.point;qe.textContent=`t=${kn(d.timestamp)} value=${on(d.value)} band=[${on(d.lower)}, ${on(d.upper)}]`+(d.isAnomaly?` \u26A0 ${d.direction} (sev ${d.severity.toFixed(2)})`:"")},onIncidentsChange:()=>{i(),at()},onThresholdChange:t=>lt(t),onLassoChange:t=>kt(t),onAlertReviewChange:(t,d)=>{let f=I.indexOf(t),k=f>=0?B[f]:[t,t];a(k[0],k[1],d==="unreviewed"?null:d),Ht()}});le.setIncidents(y),z&&le.setCaptureWindow(z);let Ht=()=>{se=D(),Me&&le.render({series:N,scored:we,params:Me,alerts:se,incidents:y}),at()},Qe=p("div","dtk-th"),Bt=p("div","dtk-th-toggles"),dt=p("button","dtk-th-toggle","Threshold capture");dt.type="button",dt.title="Mark incidents fast: set a horizontal line and grab every contiguous span above (or below) it. Click the chart to set the line, drag across it to limit the capture to a time window.",Bt.appendChild(dt);let ct=p("button","dtk-th-toggle","Lasso anomalies");ct.type="button",ct.title="Draw a freeform loop around a cloud of anomaly dots on the chart below \u2014 each run of consecutive anomalies (small gaps bridged) becomes one incident span. The ideal way to turn what the detector already flags into ground-truth incidents.",Bt.appendChild(ct),Qe.appendChild(Bt);let xe=p("div","dtk-th-bar"),et=p("select","dtk-th-sel");for(let[t,d]of[["above","above the line"],["below","below the line"]]){let f=p("option");f.value=t,f.textContent=d,et.appendChild(f)}let _e=p("input","dtk-th-num");_e.type="number",_e.step="any",_e.placeholder="hover chart";let Q=p("input","dtk-th-num");Q.type="number",Q.min="0",Q.step="1",Q.value="0";let Ot=p("span","dtk-th-scope"),ut=p("button","dtk-inc-btn","\u21BA whole view");ut.type="button",ut.style.display="none";let tt=p("button","dtk-apply-btn dtk-th-add","Add 0 spans");tt.type="button",tt.disabled=!0;let Mt=p("button","dtk-inc-btn","Done");Mt.type="button";let Ft=(t,d)=>{let f=p("label","dtk-th-grp");return f.appendChild(p("span","dtk-th-lbl",t)),f.appendChild(d),f};xe.appendChild(Ft("grab points",et)),xe.appendChild(Ft("line value",_e)),xe.appendChild(Ft("bridge gaps \u2264",Q)),xe.appendChild(Ot),xe.appendChild(ut),xe.appendChild(tt),xe.appendChild(Mt),xe.style.display="none",Qe.appendChild(xe);let je=p("div","dtk-th-bar"),Vt=p("span","dtk-th-scope","draw a loop around the anomaly dots on the chart below"),qt=p("button","dtk-inc-btn","Done");qt.type="button",je.appendChild(Vt),je.appendChild(qt),je.style.display="none",Qe.appendChild(je),Qe.style.display="none",F.appendChild(Qe);let nt=p("div","dtk-th-bar dtk-tune-reviewbar");nt.appendChild(p("span","dtk-th-scope","Click a red alert marker to confirm it valid (green) or mark it a false alarm \u2014 cycle un-reviewed \u2192 valid \u2192 false."));let jt=p("button","dtk-apply-btn dtk-th-add","Confirm all unreviewed valid");jt.type="button",jt.onclick=()=>{for(let[t,d]of B)ee(t,d)||a(t,d,"valid");Ht()};let Ut=p("button","dtk-inc-btn","Clear verdicts");Ut.type="button",Ut.onclick=()=>{ae.length=0,Ht()},nt.appendChild(jt),nt.appendChild(Ut),nt.style.display="none",F.appendChild(nt);function St(t){le.setMode(t),Object.keys(ge).forEach(d=>{var f;return(f=ge[d])==null?void 0:f.classList.toggle("on",d===t)}),t!=="label"&&(ot(!1),mt(!1)),Qe.style.display=t==="label"?"":"none",nt.style.display=t==="review"?"":"none"}let Ct=!1,Ue=!1,Tt=!1,ot=t=>{Ct=t,dt.classList.toggle("on",t),xe.style.display=t?"flex":"none",le.setThresholdMode(t),t&&Ue&&mt(!1)},mt=t=>{Ue=t,ct.classList.toggle("on",t),je.style.display=t?"flex":"none",le.setLassoMode(t),t&&Ct&&ot(!1)};dt.onclick=()=>ot(!Ct),Mt.onclick=()=>ot(!1),ct.onclick=()=>mt(!Ue),qt.onclick=()=>mt(!1),et.onchange=()=>le.setThresholdDirection(et.value),_e.oninput=()=>{let t=_e.value.trim();Tt=!0,le.setThresholdValue(t!==""&&!isNaN(Number(t))?Number(t):null),Tt=!1},Q.oninput=()=>le.setThresholdGap(Number(Q.value)||0),ut.onclick=()=>le.clearCaptureWindow(),tt.onclick=()=>{le.applyThreshold()},window.addEventListener("keydown",t=>{t.key==="Escape"&&(Ct?ot(!1):Ue&&mt(!1))}),kt=t=>{Vt.textContent=t.active?`${t.anomalies} anomal${t.anomalies===1?"y":"ies"} \u2192 ${t.incidents} incident${t.incidents===1?"":"s"} \u2014 release to add`:"draw a loop around the anomaly dots on the chart below"},lt=t=>{tt.textContent=`Add ${t.runs} span${t.runs===1?"":"s"}`,tt.disabled=t.runs===0,t.locked&&t.value!=null&&!Tt&&(_e.value=String(Math.round(t.value*1e3)/1e3)),ut.style.display=t.committed?"":"none",Ot.textContent=t.window?`scope: ${rn(t.windowMs)} painted`:"scope: current view \u2014 drag the chart to limit it"};let R=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function V(e,n,t){let o=un(e),i=o.map(m=>e[m]),r=o.map(m=>n[m]),s=C(r),c=new Array(r.length),u=0;for(let m=0;m<r.length;m++)u+=r[m],c[m]=(u-.5*r[m])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return V(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(f=>f>o),r=i.map(f=>!f),s=0,c=0;for(let f=0;f<n.length;f++)i[f]?s++:c++;if(s<2||c<2)return 0;let u=(f,M)=>M.filter((x,w)=>f[w]),d=u(r,e),m=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),a=u(i,t),A=P(d,h),p=P(m,a),S=P(N,h),g=P(y,a);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=V(n,t,25),i=V(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function j(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,m,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(m=n.upperBound)!=null?m:null,u=[];for(let y=0;y<i;y++){let h=o[y],a=r[y],A=t[y];if(Number.isNaN(a)){u.push(j(y,A,h,a,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&a<s&&(p=!0,S="below",g=s-a),c!==null&&a>c&&(p=!0,S="above",g=a-c);let f=0;if(p)if(s!==null&&c!==null){let T=c-s;f=T>0?g/T:1/0}else f=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:a,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:f,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),m=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let a=0;a<c;a++){let A=s[a],p=d[a],S=r[a];if(Number.isNaN(p)){h.push(j(a,S,A,p,"missing_data"));continue}let g=Math.max(0,a-n.windowSize),f=a-g,M=d.slice(g,a),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(j(a,S,A,p,"insufficient_data"));continue}let T=new Array(f);for(let l=0;l<f;l++)T[l]=f-l;let z=[];for(let l=0;l<f;l++)x[l]&&z.push(T[l]);let B=Q(z,y),W=0;n.detrend==="linear"&&(W=Sn(w,z,B));let nn=W!==0?w.map((l,O)=>l+W*z[O]):w,G=U(t,nn,B),I={...G};if(m&&N){let l=N[a];for(let O of n.seasonalityComponents){let K=new Array(f).fill(!0);for(let b=0;b<f;b++){let q=N[g+b],v=!0;for(let E of O)if(!q||q[E]!==(l==null?void 0:l[E])){v=!1;break}K[b]=v}let L=[],F=[];for(let b=0;b<f;b++)x[b]&&K[b]&&(L.push(M[b]),F.push(T[b]));if(L.length<n.minSamplesPerGroup)continue;let tn=Q(F,y),on=W!==0?L.map((b,q)=>b+W*F[q]):L,rn=U(t,on,tn);for(let[b,q]of i){let v=G[b],sn=(q==="spread"?v>0:v!==0)?rn[b]/v:1;I[b]*=sn}}}let[D,k]=Nn(t,I,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,R=null,J=0;if(H){let l;p<D?(R="below",l=D-p):(R="above",l=p-k),J=An(t,I,l)}let en=Mn(t,I);h.push({index:a,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:R,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var _=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function Tn(e,n,t){let o=[],i=e.length,r=0;for(;r<i;){if(!(e[r].scored&&e[r].isAnomaly)){r++;continue}let s=r;for(;s+1<i&&e[s+1].scored&&e[s+1].isAnomaly&&e[s+1].timestamp-e[s].timestamp===n;)s++;s-r+1>=t&&o.push({fire:r+t-1,startTs:e[r].timestamp,endTs:e[s].timestamp}),r=s+1}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){_=n.series;return}if(n.type==="run"&&_){let t=n.params,o=Z(_,t);xn(o,t.direction);let i=_.intervalSeconds*1e3,r=Tn(o,i,t.consecutiveAnomalies),s=r.map(m=>m.fire),c=r.map(m=>[m.startTs,m.endTs]),u=$(_,t),d=0;for(let m of o)m.scored&&m.isAnomaly&&d++;self.postMessage({type:"result",id:n.id,scored:o,fires:s,fireSpans:c,eff:u,flagged:d})}};})();
2
+ `],{type:"text/javascript"})));R.postMessage({type:"series",series:N});let $e=0,Me=null;R.onmessage=t=>{let d=t.data;if(d.type!=="result"||d.id!==$e||!Me)return;ue.classList.remove("on");let f=Me;we=d.scored,I=d.fires.map(k=>N.timestamps[k]),B=d.fireSpans,se=D(),le.render({series:N,scored:d.scored,params:f,alerts:se,incidents:y}),at(),m.textContent=`${d.flagged} flagged \xB7 ${d.fires.length} alert${d.fires.length===1?"":"s"} \xB7 warm-up ${d.eff} pts`,mn(f),P.textContent=qn(f,ne)},R.onerror=()=>{ue.classList.remove("on"),m.textContent="recompute failed \u2014 see the browser console"};let Le=()=>{Me=Y(),$e+=1,ue.classList.add("on"),R.postMessage({type:"run",id:$e,params:Me})},me=t=>t>1?rn(w.timestamps[e-1]-w.timestamps[e-t]):"\u2014",rt=t=>{Ve.textContent=t>=e?`${e} pts \xB7 full (${me(e)})`:`${t} pts \xB7 ${me(t)}`};rt(e);function fn(t){N=G(t),R.postMessage({type:"series",series:N}),pe()}let At=0;L.oninput=()=>{let t=Number(L.value);rt(t),At&&window.clearTimeout(At),At=window.setTimeout(()=>fn(t),200)};let Dt=0,pe=()=>{Dt&&window.clearTimeout(Dt),Dt=window.setTimeout(Le,130)},ft=nn("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],E.type,t=>{var d;pn((d=On[t])!=null?d:3),u(),pe()},"The statistic for the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles) \u2014 all windowed \u2014 or Manual (fixed lower/upper thresholds, no window/history).");J.appendChild(ft.row);let Lt=Wt("Lower bound",{min:be,max:yt,step:Rt,value:Re,fmt:t=>on(t),hint:"Manual bounds: values below this read as anomalous. Drag in from the data range to see how many points fall outside (and how many alerts that yields)."},pe);J.appendChild(Lt.row);let Pt=Wt("Upper bound",{min:be,max:yt,step:Rt,value:Oe,fmt:t=>on(t),hint:"Manual bounds: values above this read as anomalous."},pe);J.appendChild(Pt.row);let it=Wt("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:E.threshold,fmt:t=>t.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},pe);J.appendChild(it.row);let Gt=it.row.querySelector("input"),Yt=it.row.querySelector(".dtk-ctl-val"),pn=t=>{Gt&&(Gt.value=String(t)),Yt&&(Yt.textContent=t.toFixed(1))},hn=Math.max(50,Math.min(2e3,Math.floor(e/2))),an=Math.max(hn,E.windowSize),Kt=Wt("Window size (points)",{min:Math.max(1,Math.min(10,E.windowSize)),max:an,step:1,value:E.windowSize,fmt:t=>`${t} \xB7 ${rn(t*n.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},pe);J.appendChild(Kt.row);let pt=nn("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],E.windowWeights,()=>{u(),pe()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");J.appendChild(pt.row);let ln=Wt("Half-life (points)",{min:1,max:an,step:1,value:(re=E.halfLife)!=null?re:Math.max(5,Math.round(E.windowSize/20)),fmt:t=>`${t} \xB7 ${rn(t*n.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},pe),Xt=ln.row;Xt.style.display=E.windowWeights==="exponential"?"":"none",J.appendChild(Xt);let Jt=nn("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],E.detrend,pe,"Remove a robust linear trend from each window before computing the band, so a steadily rising/falling metric is not flagged for the trend itself.");J.appendChild(Jt.row);let Zt=nn("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],E.smoothing,pe,"Smooth the series before detection (EMA or SMA) so single-point jitter does not flag. The detector judges the smoothed line; the raw values show as a faint ghost.");J.appendChild(Zt.row);let Qt=null;if(n.seasonality_columns.length){let t=n.seasonality_columns,d=p("div","dtk-ctl");Qt=d,d.appendChild(sn("Seasonality groups","Condition the band on seasonal keys. Pick a group per column: columns in the SAME group are combined into one key (e.g. dow\xD7hour); separate groups each apply their own correction. Off = ignore that column."));let f=Math.min(t.length,6),k=[{label:"\u2014",value:"0"}];for(let C=1;C<=f;C++)k.push({label:`G${C}`,value:String(C)});t.forEach(C=>{let _=p("div","dtk-season-row");_.appendChild(p("span","dtk-season-col",C));let ie=p("div","dtk-seg dtk-season-seg"),ht=[],$t=()=>{var Ce;return(Ce=de.get(C))!=null?Ce:0},en=()=>ht.forEach(Ce=>Ce.classList.toggle("on",Number(Ce.dataset.v)===$t()));k.forEach(Ce=>{let He=p("button","dtk-seg-btn",Ce.label);He.type="button",He.dataset.v=Ce.value,He.title=Ce.value==="0"?`ignore ${C}`:`put ${C} in group ${Ce.value}`,He.onclick=()=>{de.set(C,Number(Ce.value)),en(),pe()},ht.push(He),ie.appendChild(He)}),en(),_.appendChild(ie),d.appendChild(_)}),J.appendChild(d)}let dn=nn("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(Se=n.direction)!=null?Se:"any",pe,"Which anomalies to show and count toward alerts: both directions, only spikes ABOVE the band (up) or only drops BELOW it (down). A preview filter mirroring the alert direction policy \u2014 it never changes the band itself.");J.appendChild(dn.row);let bn=Wt("Alert: consecutive anomalies",{min:1,max:10,step:1,value:ne,fmt:t=>String(t),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},t=>{ne=t,pe()});J.appendChild(bn.row);let cn=p("div","dtk-ctl"),Nt=p("label","dtk-check"),Et=p("input");Et.type="checkbox",Et.onchange=()=>{le.setZeroLine(Et.checked)},Nt.title="Draw a horizontal line at y = 0 and include zero in the scale \u2014 for real-valued metrics best read relative to zero.",Nt.appendChild(Et),Nt.appendChild(document.createTextNode(" Show y = 0 line")),cn.appendChild(Nt),J.appendChild(cn);let _t=p("div","dtk-ctl dtk-incidents");_t.appendChild(sn("Marked incidents","The real incidents you marked (in Label mode). Edit a label, focus to zoom the chart to it, or remove it. Save the set below to incidents/<metric>/ \u2014 the same store dtk autotune reads."));let o=p("div","dtk-inc-list");_t.appendChild(o),J.appendChild(_t);function r(t){let d=Math.max((t.end-t.start)*.5,n.interval_seconds*1e3*5);le.setViewWindow(t.start-d,t.end+d)}function l(t){let d=y.indexOf(t);d>=0&&y.splice(d,1),le.setIncidents(y),i(),at()}function i(){o.innerHTML="";let t=[...y].sort((d,f)=>d.start-f.start);if(!t.length){o.appendChild(p("div","dtk-inc-empty","None yet \u2014 switch to Label mode and drag across the chart, or confirm alerts in Review mode."));return}for(let d of t){let f=p("div","dtk-inc-row");f.appendChild(p("span","dtk-inc-span",`${kn(d.start)} \u2192 ${kn(d.end)}`)),f.appendChild(p("span","dtk-inc-dur",rn(Math.max(0,d.end-d.start))));let k=p("input","dtk-inc-label");k.type="text",k.value=d.label||"",k.placeholder="label (optional)",k.oninput=()=>{d.label=k.value},f.appendChild(k);let C=p("button","dtk-inc-btn","focus");C.type="button",C.onclick=()=>r(d),f.appendChild(C);let _=p("button","dtk-inc-btn dtk-inc-del","\u2715");_.type="button",_.title="remove this incident",_.onclick=()=>l(d),f.appendChild(_),o.appendChild(f)}}let s=[it.row,Kt.row,pt.row,Jt.row,Zt.row];Qt&&s.push(Qt);function u(){let t=ft.get()==="manual_bounds";for(let d of s)d.style.display=t?"none":"";Lt.row.style.display=t?"":"none",Pt.row.style.display=t?"":"none",Xt.style.display=!t&&pt.get()==="exponential"?"":"none"}u();let m=p("div","dtk-tune-stat");F.appendChild(m);let v=p("div","dtk-tune-cfg");v.appendChild(p("span","dtk-tune-cfg-k","// effective config"));let P=p("code","dtk-tune-cfg-v");if(v.appendChild(P),F.appendChild(v),n.save_url){let t=p("div","dtk-tune-apply"),d=p("button","dtk-apply-btn","Apply to metric");d.type="button",d.title="Write this detector config back into the metric YAML (the previous version is archived under metrics/.history/). Trimming the sample does not change what is written.";let f=p("span","dtk-apply-msg");d.onclick=()=>{let k=Y();d.disabled=!0,f.className="dtk-apply-msg info",f.textContent="Applying\u2026",fetch(n.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:k.type,params:_n(k)},consecutive_anomalies:ne})}).then(C=>C.ok?C.json():C.text().then(_=>{throw new Error(_||`HTTP ${C.status}`)})).then(C=>{var _;f.className="dtk-apply-msg ok",f.textContent=`Applied \u2192 ${(_=C.saved)!=null?_:"metric"} (previous archived). You can close this tab.`}).catch(C=>{d.disabled=!1,f.className="dtk-apply-msg err",f.textContent=`Apply failed: ${C.message}`})},t.appendChild(d),t.appendChild(f),F.appendChild(t)}else F.appendChild(p("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));let S=t=>'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',T=t=>new Date(t).toISOString().slice(0,19).replace("T"," "),q=()=>{let t=[`metric: ${n.metric}`,"timezone: UTC"],d=ae.filter(_=>_.verdict==="valid").map(_=>({start:_.start,end:_.end,label:"alert (confirmed)"})),f=new Set,k=[...y,...d].sort((_,ie)=>_.start-ie.start).filter(_=>{let ie=`${Math.round(_.start)}:${Math.round(_.end)}`;return f.has(ie)?!1:(f.add(ie),!0)});if(!k.length)t.push("incidents: []");else{t.push("incidents:");for(let _ of k){let ie=_.label?`, label: ${S(_.label)}`:"";t.push(` - {start: "${T(_.start)}", end: "${T(_.end)}"${ie}}`)}}let C=le.getCaptureWindow();if(C&&(t.push("capture_windows:"),t.push(` - {start: "${T(C.start)}", end: "${T(C.end)}"}`)),ae.length){t.push("alert_reviews:");for(let _ of[...ae].sort((ie,ht)=>ie.start-ht.start))t.push(` - {start: "${T(_.start)}", end: "${T(_.end)}", verdict: ${_.verdict}}`)}return t.join(`
3
+ `)+`
4
+ `},K=p("div","dtk-tune-apply"),fe=p("input","dtk-setname");fe.type="text",fe.placeholder="name this set (optional)";let X=p("button","dtk-apply-btn dtk-labels-btn",n.labels_save_url?"Save incidents":"Download incidents");X.type="button",X.title="Write the marked incidents to incidents/<metric>/ (versioned) so they also feed the next `dtk autotune`. Saving labels does not end tuning \u2014 keep adjusting and apply the detector when ready.";let x=p("span","dtk-apply-msg");X.onclick=()=>{let t=q();if(!n.labels_save_url){let d=new Blob([t],{type:"text/yaml"}),f=p("a");f.href=URL.createObjectURL(d),f.download=`${n.metric}.yml`,f.click(),URL.revokeObjectURL(f.href),x.className="dtk-apply-msg ok",x.textContent=`Downloaded \u2014 drop it into incidents/${n.metric}/`;return}x.className="dtk-apply-msg info",x.textContent="Saving\u2026",fetch(n.labels_save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:fe.value,yaml:t})}).then(d=>d.ok?d.json():d.text().then(f=>{throw new Error(f||`HTTP ${d.status}`)})).then(d=>{var f;x.className="dtk-apply-msg ok",x.textContent=`Saved \u2192 ${(f=d.saved)!=null?f:"incidents"} (keep tuning, or Apply the detector)`}).catch(d=>{x.className="dtk-apply-msg err",x.textContent=`Save failed: ${d.message}`})},K.appendChild(fe),K.appendChild(X),K.appendChild(x),F.appendChild(K),St("tune"),i(),Le(),at();let $=0;window.addEventListener("resize",()=>{$&&cancelAnimationFrame($),$=requestAnimationFrame(()=>{le.resize()})})}function on(n){if(!Number.isFinite(n))return"\u2014";let c=Math.abs(n);return c!==0&&(c<.01||c>=1e6)?n.toExponential(2):Number(n.toFixed(c<1?4:2)).toString()}function kn(n){return new Date(n).toISOString().slice(0,16).replace("T"," ")}function rn(n){let c=Math.round(n/6e4);if(c<60)return`${c}m`;let b=Math.floor(c/60);if(b<24)return`${b}h`;let e=Math.floor(b/24),w=b%24;return w?`${e}d ${w}h`:`${e}d`}function Un(n){return n%86400===0?`${n/86400}d`:n%3600===0?`${n/3600}h`:n%60===0?`${n/60}min`:`${n}s`}var En=!1;function Gn(){if(En)return;En=!0;let n=`
5
+ .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
6
+ --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
7
+ --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
8
+ --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
9
+ .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
10
+ .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
11
+ .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
12
+ .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
13
+ color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
14
+ .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
15
+ .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
16
+ .dtk-tune-dock{margin-top:14px;}
17
+ .dtk-tune-dockhead{display:flex;align-items:center;gap:10px;margin-bottom:8px;}
18
+ .dtk-dock-toggle{border:1px solid var(--border);background:var(--surface);color:var(--ink);border-radius:8px;
19
+ padding:7px 14px;font-family:var(--sans);font-size:13px;font-weight:600;cursor:pointer;}
20
+ .dtk-dock-toggle:hover{border-color:var(--c);color:var(--c7);}
21
+ .dtk-tune-controls{display:grid;grid-template-columns:repeat(auto-fill,minmax(228px,1fr));gap:14px 22px;
22
+ background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;align-items:start;}
23
+ .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
24
+ .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
25
+ .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
26
+ .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
27
+ .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
28
+ .dtk-seg.dtk-wrap{flex-wrap:wrap;}
29
+ .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
30
+ font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
31
+ .dtk-seg-btn:hover{color:var(--ink);}
32
+ .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
33
+ .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
34
+ .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
35
+ .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;margin-top:14px;}
36
+ .dtk-tune-chart{position:relative;width:100%;height:min(56vh,560px);min-height:420px;background:var(--surface);
37
+ border:1px solid var(--border);border-radius:12px;overflow:hidden;}
38
+ .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
39
+ .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
40
+ .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
41
+ .dtk-tune-warn{font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--c7);
42
+ background:rgba(240,173,78,0.13);border:1px solid rgba(240,173,78,0.5);border-radius:8px;padding:8px 11px;}
43
+ .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
44
+ font-size:12px;overflow-x:auto;}
45
+ .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
46
+ .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
47
+ .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
48
+ .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
49
+ font-size:14px;font-weight:600;cursor:pointer;}
50
+ .dtk-apply-btn:hover{background:var(--c7);}
51
+ .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
52
+ .dtk-apply-msg{font-size:13px;}
53
+ .dtk-apply-msg.ok{color:var(--green);}
54
+ .dtk-apply-msg.err{color:var(--anom);}
55
+ .dtk-apply-msg.info{color:var(--muted);}
56
+ .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
57
+ border-radius:8px;padding:10px 12px;}
58
+ .dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
59
+ .dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
60
+ border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
61
+ .dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
62
+ .dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
63
+ .dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
64
+ background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
65
+ padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
66
+ .dtk-tune-spin.on{display:inline-flex;}
67
+ .dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
68
+ border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
69
+ @keyframes dtk-spin{to{transform:rotate(360deg);}}
70
+ .dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
71
+ .dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
72
+ .dtk-leg-sw{display:inline-block;flex:0 0 auto;}
73
+ .dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
74
+ .dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
75
+ border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
76
+ .dtk-leg-sw.center{width:16px;height:2px;
77
+ background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
78
+ .dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
79
+ .dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
80
+ border-top:7px solid var(--anom);}
81
+ .dtk-leg-sw.alert-ok{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
82
+ border-top:7px solid var(--green);}
83
+ .dtk-leg-sw.alert-no{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
84
+ border-top:7px solid #5a7a8c;}
85
+ .dtk-leg-txt{white-space:nowrap;}
86
+ .dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
87
+ .dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
88
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
89
+ .dtk-season-seg{flex:0 0 auto;padding:2px;}
90
+ .dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
91
+ .dtk-tune-metrics{display:flex;flex-wrap:wrap;gap:10px;margin:16px 0 2px;}
92
+ .dtk-m-chip{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;background:var(--surface);
93
+ border:1px solid var(--border);border-radius:10px;font-size:13px;}
94
+ .dtk-m-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;}
95
+ .dtk-m-v{font-family:var(--mono);font-weight:700;font-size:15px;color:var(--ink);}
96
+ .dtk-m-l{color:var(--faint);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
97
+ .dtk-m-sub{color:var(--muted);font-family:var(--mono);font-size:11.5px;}
98
+ .dtk-tune-labhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
99
+ margin-top:4px;flex-wrap:wrap;}
100
+ .dtk-tune-labhint{font-family:var(--mono);font-size:11px;color:var(--faint);}
101
+ .dtk-tune-labchart{height:240px;}
102
+ .dtk-tune-modes{display:inline-flex;gap:4px;background:var(--ink);border-radius:9px;padding:4px;margin:2px 0;align-self:flex-start;}
103
+ .dtk-mode-btn{border:0;background:transparent;color:#c9c2b4;font-family:var(--sans);font-size:13px;font-weight:600;
104
+ padding:7px 16px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}
105
+ .dtk-mode-btn:hover{color:#fff;}
106
+ .dtk-mode-btn.on{background:var(--c);color:#fff;}
107
+ .dtk-tune-reviewbar{align-items:center;}
108
+ .dtk-tune-reviewbar .dtk-apply-btn{background:var(--green);}
109
+ .dtk-tune-reviewbar .dtk-apply-btn:hover{background:#27815d;}
110
+ .dtk-th{display:flex;flex-direction:column;gap:8px;margin:2px 0 6px;}
111
+ .dtk-th-toggles{display:flex;gap:8px;flex-wrap:wrap;}
112
+ .dtk-th-toggle{align-self:flex-start;border:1px solid var(--border);background:var(--surface);
113
+ color:var(--muted);border-radius:8px;padding:6px 12px;font-family:var(--sans);font-size:12.5px;cursor:pointer;}
114
+ .dtk-th-toggle:hover{border-color:var(--c);color:var(--c7);}
115
+ .dtk-th-toggle.on{background:var(--c);border-color:var(--c);color:#fff;}
116
+ .dtk-th-bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 14px;padding:11px 13px;
117
+ background:var(--surface);border:1px solid var(--border);border-radius:10px;}
118
+ .dtk-th-grp{display:flex;flex-direction:column;gap:3px;}
119
+ .dtk-th-lbl{font-family:var(--mono);font-size:10.5px;color:var(--faint);text-transform:uppercase;letter-spacing:.05em;}
120
+ .dtk-th-num,.dtk-th-sel{background:var(--paper);color:var(--ink);border:1px solid var(--border);
121
+ border-radius:6px;padding:5px 8px;font-family:var(--mono);font-size:12px;}
122
+ .dtk-th-num{width:96px;}
123
+ .dtk-th-num:focus,.dtk-th-sel:focus{outline:none;border-color:var(--c);}
124
+ .dtk-th-scope{font-family:var(--mono);font-size:11px;color:var(--muted);align-self:center;flex:1 1 160px;}
125
+ .dtk-th-add{padding:7px 14px;}
126
+ .dtk-th-add:disabled{opacity:.5;cursor:default;}
127
+ .dtk-incidents{gap:8px;grid-column:1/-1;}
128
+ .dtk-inc-list{display:flex;flex-direction:column;gap:6px;max-height:200px;overflow:auto;}
129
+ .dtk-inc-empty{font-size:12px;color:var(--faint);font-style:italic;}
130
+ .dtk-inc-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:var(--paper);
131
+ border:1px solid var(--border);border-radius:7px;padding:6px 8px;}
132
+ .dtk-inc-span{font-family:var(--mono);font-size:10.5px;color:var(--ink);}
133
+ .dtk-inc-dur{font-family:var(--mono);font-size:10.5px;color:var(--muted);}
134
+ .dtk-inc-label{flex:1 1 90px;min-width:70px;background:var(--surface);color:var(--ink);
135
+ border:1px solid var(--border);border-radius:5px;padding:4px 7px;font-family:var(--sans);font-size:11.5px;}
136
+ .dtk-inc-label:focus{outline:none;border-color:var(--c);}
137
+ .dtk-inc-btn{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;
138
+ padding:3px 8px;font-size:11px;cursor:pointer;font-family:var(--sans);}
139
+ .dtk-inc-btn:hover{border-color:var(--c);color:var(--c7);}
140
+ .dtk-inc-del{color:var(--anom);}
141
+ .dtk-setname{background:var(--surface);color:var(--ink);border:1px solid var(--border);border-radius:8px;
142
+ padding:9px 11px;font-family:var(--sans);font-size:13px;min-width:180px;}
143
+ .dtk-setname::placeholder{color:var(--faint);}
144
+ .dtk-setname:focus{outline:none;border-color:var(--c);}
145
+ .dtk-labels-btn{background:var(--surface);color:var(--ink);border:1px solid var(--border);}
146
+ .dtk-labels-btn:hover{background:var(--paper);border-color:var(--c);color:var(--c7);}
147
+ `,c=document.createElement("style");c.textContent=n,document.head.appendChild(c)}window.__DTK_TUNE__={render:jn};})();
@@ -177,6 +177,7 @@ def build_tune_payload(
177
177
  save_url: str | None = None,
178
178
  incidents: list[dict[str, str]] | None = None,
179
179
  capture_windows: list[dict[str, str]] | None = None,
180
+ alert_reviews: list[dict[str, str]] | None = None,
180
181
  ) -> dict[str, Any]:
181
182
  """Assemble the interactive tuning payload from the persisted ``_dtk_datapoints``.
182
183
 
@@ -198,6 +199,7 @@ def build_tune_payload(
198
199
  """
199
200
  seed_incidents = incidents or []
200
201
  seed_capture = capture_windows or []
202
+ seed_reviews = alert_reviews or []
201
203
  name = metric_config.name
202
204
  interval = metric_config.get_interval()
203
205
  interval_seconds = interval.seconds
@@ -260,6 +262,7 @@ def build_tune_payload(
260
262
  "save_url": save_url,
261
263
  "incidents": seed_incidents,
262
264
  "capture_windows": seed_capture,
265
+ "alert_reviews": seed_reviews,
263
266
  "labels_save_url": None,
264
267
  }
265
268
  if start is None or end is None:
@@ -301,5 +304,6 @@ def build_tune_payload(
301
304
  "save_url": save_url,
302
305
  "incidents": seed_incidents,
303
306
  "capture_windows": seed_capture,
307
+ "alert_reviews": seed_reviews,
304
308
  "labels_save_url": None,
305
309
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.37.0
3
+ Version: 0.38.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT