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.
- {detectkit-0.37.0/detectkit.egg-info → detectkit-0.38.0}/PKG-INFO +1 -1
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/__init__.py +1 -1
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/labels.py +48 -1
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/cli.md +30 -23
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/tune.py +6 -0
- detectkit-0.38.0/detectkit/tuning/assets/tune.js +147 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/payload.py +4 -0
- {detectkit-0.37.0 → detectkit-0.38.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.37.0/detectkit/tuning/assets/tune.js +0 -132
- {detectkit-0.37.0 → detectkit-0.38.0}/LICENSE +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/MANIFEST.in +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/README.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/html_labeler.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/core/models.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/pyproject.toml +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/requirements.txt +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/setup.cfg +0 -0
- {detectkit-0.37.0 → detectkit-0.38.0}/setup.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`capture_windows`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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&<(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&<(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
|
}
|