detectkit 0.33.0__tar.gz → 0.34.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. {detectkit-0.33.0/detectkit.egg-info → detectkit-0.34.0}/PKG-INFO +1 -1
  2. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/label_server.py +6 -21
  4. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/labels.py +41 -0
  5. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/cli.md +14 -3
  6. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/tune.py +25 -0
  7. detectkit-0.34.0/detectkit/reporting/assets/report.js +80 -0
  8. detectkit-0.34.0/detectkit/tuning/assets/tune.js +117 -0
  9. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/tuning/payload.py +11 -0
  10. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/tuning/server.py +68 -14
  11. {detectkit-0.33.0 → detectkit-0.34.0/detectkit.egg-info}/PKG-INFO +1 -1
  12. detectkit-0.33.0/detectkit/reporting/assets/report.js +0 -77
  13. detectkit-0.33.0/detectkit/tuning/assets/tune.js +0 -82
  14. {detectkit-0.33.0 → detectkit-0.34.0}/LICENSE +0 -0
  15. {detectkit-0.33.0 → detectkit-0.34.0}/MANIFEST.in +0 -0
  16. {detectkit-0.33.0 → detectkit-0.34.0}/README.md +0 -0
  17. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/__init__.py +0 -0
  18. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/__init__.py +0 -0
  19. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/base.py +0 -0
  20. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/branding.py +0 -0
  21. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/email.py +0 -0
  22. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/factory.py +0 -0
  23. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/mattermost.py +0 -0
  24. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/slack.py +0 -0
  25. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/telegram.py +0 -0
  26. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/channels/webhook.py +0 -0
  27. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  28. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  29. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  30. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  31. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  32. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  33. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  34. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  35. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  36. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/__init__.py +0 -0
  37. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/_base.py +0 -0
  38. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/_types.py +0 -0
  39. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/autotuner.py +0 -0
  40. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/config_emitter.py +0 -0
  41. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/crossval.py +0 -0
  42. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/detector_select.py +0 -0
  43. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/distribution.py +0 -0
  44. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/grid_search.py +0 -0
  45. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/html_labeler.py +0 -0
  46. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/result.py +0 -0
  47. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/scoring.py +0 -0
  48. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/seasonality_search.py +0 -0
  49. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/settings.py +0 -0
  50. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/autotune/window_select.py +0 -0
  51. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/__init__.py +0 -0
  52. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/_output.py +0 -0
  53. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  54. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  55. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  56. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  57. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  58. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  59. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  60. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  61. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  62. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  63. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  64. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/__init__.py +0 -0
  65. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/autotune.py +0 -0
  66. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/clean.py +0 -0
  67. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/init.py +0 -0
  68. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/init_claude.py +0 -0
  69. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/run.py +0 -0
  70. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/test_alert.py +0 -0
  71. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/commands/unlock.py +0 -0
  72. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/cli/main.py +0 -0
  73. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/config/__init__.py +0 -0
  74. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/config/metric_config.py +0 -0
  75. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/config/profile.py +0 -0
  76. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/config/project_config.py +0 -0
  77. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/config/validator.py +0 -0
  78. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/core/__init__.py +0 -0
  79. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/core/interval.py +0 -0
  80. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/core/models.py +0 -0
  81. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/__init__.py +0 -0
  82. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/_sql_manager.py +0 -0
  83. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/clickhouse_manager.py +0 -0
  84. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/__init__.py +0 -0
  85. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  86. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  87. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_base.py +0 -0
  88. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  89. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_detections.py +0 -0
  90. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  91. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  92. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_schema.py +0 -0
  93. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  94. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/internal_tables/manager.py +0 -0
  95. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/manager.py +0 -0
  96. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/mysql_manager.py +0 -0
  97. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/postgres_manager.py +0 -0
  98. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/database/tables.py +0 -0
  99. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/__init__.py +0 -0
  100. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/base.py +0 -0
  101. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/factory.py +0 -0
  102. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/seasonality.py +0 -0
  103. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/__init__.py +0 -0
  104. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  105. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/iqr.py +0 -0
  106. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/mad.py +0 -0
  107. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  108. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/detectors/statistical/zscore.py +0 -0
  109. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/loaders/__init__.py +0 -0
  110. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/loaders/metric_loader.py +0 -0
  111. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/loaders/query_template.py +0 -0
  112. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/__init__.py +0 -0
  113. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/error_dispatch.py +0 -0
  114. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  115. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  116. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  117. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  118. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  119. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  120. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  121. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/reporting/__init__.py +0 -0
  122. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/reporting/builder.py +0 -0
  123. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/reporting/html_report.py +0 -0
  124. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/tuning/__init__.py +0 -0
  125. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/tuning/config_writer.py +0 -0
  126. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/tuning/html.py +0 -0
  127. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/utils/__init__.py +0 -0
  128. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/utils/datetime_utils.py +0 -0
  129. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/utils/env_interpolation.py +0 -0
  130. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/utils/json_utils.py +0 -0
  131. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit/utils/stats.py +0 -0
  132. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit.egg-info/SOURCES.txt +0 -0
  133. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit.egg-info/dependency_links.txt +0 -0
  134. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit.egg-info/entry_points.txt +0 -0
  135. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit.egg-info/requires.txt +0 -0
  136. {detectkit-0.33.0 → detectkit-0.34.0}/detectkit.egg-info/top_level.txt +0 -0
  137. {detectkit-0.33.0 → detectkit-0.34.0}/pyproject.toml +0 -0
  138. {detectkit-0.33.0 → detectkit-0.34.0}/requirements.txt +0 -0
  139. {detectkit-0.33.0 → detectkit-0.34.0}/setup.cfg +0 -0
  140. {detectkit-0.33.0 → detectkit-0.34.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.33.0
3
+ Version: 0.34.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.33.0"
7
+ __version__ = "0.34.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -10,12 +10,10 @@ off the machine, and nothing is written until the user explicitly saves.
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
- import re
14
13
  import secrets
15
14
  import threading
16
15
  import webbrowser
17
16
  from collections.abc import Callable
18
- from datetime import datetime, timezone
19
17
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
20
18
  from pathlib import Path
21
19
  from typing import Any, cast
@@ -24,23 +22,12 @@ from urllib.parse import parse_qs, urlparse
24
22
  import numpy as np
25
23
 
26
24
  from detectkit.autotune.html_labeler import render_labeler_html
27
- from detectkit.autotune.labels import parse_incident_labels
28
-
29
- _NAME_RE = re.compile(r"[^a-z0-9_-]+")
30
- _MAX_BODY = 5_000_000 # generous cap on the posted labels payload
25
+ from detectkit.autotune.labels import parse_incident_labels, versioned_labels_path
31
26
 
27
+ # Re-exported under the historical name for the labeler-server tests.
28
+ from detectkit.autotune.labels import sanitize_label_set_name as _sanitize # noqa: F401
32
29
 
33
- def _sanitize(name: str) -> str:
34
- """Filesystem-safe slug for a label-set name; ``""`` when blank.
35
-
36
- Used as an optional *suffix* appended to the metric-named filename, so a blank
37
- name simply yields no suffix (the file is named after the metric alone).
38
- """
39
- return _NAME_RE.sub("-", name.strip().lower()).strip("-")
40
-
41
-
42
- def _stamp() -> str:
43
- return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
30
+ _MAX_BODY = 5_000_000 # generous cap on the posted labels payload
44
31
 
45
32
 
46
33
  class _LabelServer(ThreadingHTTPServer):
@@ -90,16 +77,14 @@ class _Handler(BaseHTTPRequestHandler):
90
77
 
91
78
  payload = json.loads(self.rfile.read(length).decode("utf-8"))
92
79
  yaml_text = str(payload.get("yaml", ""))
93
- suffix = _sanitize(str(payload.get("name", "")))
94
80
  raw = _yaml.safe_load(yaml_text)
95
81
  # validate against the canonical schema before writing anything
96
82
  parse_incident_labels(
97
83
  raw, interval_seconds=srv.interval_seconds, metric_name=srv.metric
98
84
  )
99
85
  srv.incidents_dir.mkdir(parents=True, exist_ok=True)
100
- # Always name the file after the metric; the optional set name is a suffix.
101
- stem = f"{srv.metric}-{suffix}" if suffix else srv.metric
102
- out = srv.incidents_dir / f"{stem}-{_stamp()}.yml"
86
+ # Versioned, metric-named file (the optional set name is a suffix).
87
+ out = versioned_labels_path(srv.incidents_dir, srv.metric, str(payload.get("name", "")))
103
88
  out.write_text(yaml_text, encoding="utf-8")
104
89
  srv.saved_path = out
105
90
  except Exception as exc:
@@ -14,6 +14,7 @@ When no labels are supplied the tuner falls back to unsupervised mode.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import re
17
18
  from dataclasses import dataclass, field
18
19
  from datetime import datetime, timezone
19
20
  from pathlib import Path
@@ -279,3 +280,43 @@ def load_capture_windows(
279
280
  """Load a labels file and render its capture windows as labeler display dicts."""
280
281
  labels = parse_labels_file(path, interval_seconds=interval_seconds, metric_name=metric_name)
281
282
  return capture_windows_to_display(labels)
283
+
284
+
285
+ # Versioned labels store helpers (shared by the autotune labeler server and the
286
+ # `dtk tune` labeler so both name + discover files the same way).
287
+ _LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
288
+ _NAME_RE = re.compile(r"[^a-z0-9_-]+")
289
+
290
+
291
+ def newest_labels_file(directory: str | Path) -> Path | None:
292
+ """Newest versioned labels file in *directory* (``None`` if empty/absent).
293
+
294
+ Versioned names (``<metric>[-<set>]-<UTCstamp>.yml``) sort chronologically by
295
+ name; mtime tie-breaks.
296
+ """
297
+ d = Path(directory)
298
+ if not d.is_dir():
299
+ return None
300
+ files: list[Path] = []
301
+ for pattern in _LABELS_GLOBS:
302
+ files.extend(d.glob(pattern))
303
+ if not files:
304
+ return None
305
+ return sorted(files, key=lambda p: (p.name, p.stat().st_mtime))[-1]
306
+
307
+
308
+ def sanitize_label_set_name(name: str) -> str:
309
+ """Filesystem-safe slug for an optional label-set name (``""`` when blank)."""
310
+ return _NAME_RE.sub("-", name.strip().lower()).strip("-")
311
+
312
+
313
+ def versioned_labels_path(incidents_dir: str | Path, metric: str, name: str = "") -> Path:
314
+ """Versioned labels path ``<metric>[-<slug>]-<UTCstamp>.yml`` in *incidents_dir*.
315
+
316
+ The single source of the labels filename convention: the optional set *name*
317
+ is sanitized to a suffix, blank → just the metric name.
318
+ """
319
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
320
+ slug = sanitize_label_set_name(name)
321
+ stem = f"{metric}-{slug}" if slug else metric
322
+ return Path(incidents_dir) / f"{stem}-{stamp}.yml"
@@ -88,13 +88,24 @@ config back into the metric YAML **in place** (autotune, by contrast, writes a n
88
88
  datapoints (run `dtk run --steps load` first if empty); the selector must resolve
89
89
  to a single metric.
90
90
 
91
+ **Mark incidents + see alert quality live.** A second, **synced** chart beneath
92
+ the detector view lets you **mark the real incidents** (drag to create a span,
93
+ drag its edges/middle to adjust, ✕ or Delete to remove). As you tune, a metrics
94
+ bar shows two operator numbers: **incident catch rate (recall)** — how many marked
95
+ incidents your config catches — and **false-alert rate** — what share of alerts
96
+ fall outside any real incident ("≈1 in N false"). **Save incidents** writes a
97
+ versioned `incidents/<metric>/*.yml` — the same store `dtk autotune` reads, so the
98
+ same labels feed the next supervised tune (it seeds from the newest file on open).
99
+ Saving incidents does not end the session; only **Apply** does. A **y = 0 line**
100
+ toggle (shared with `dtk run --report`) shows the metric relative to zero.
101
+
91
102
  Safe write-back: the config is validated before anything is written, the previous
92
103
  YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
93
104
  overwritten. Takes **no pipeline lock** (it only edits a config file); re-run
94
105
  `dtk run` afterward to recompute detections under the new config.
95
- `--no-serve` writes a static read-only preview HTML instead (no write-back);
96
- `--from` / `--to` bound the window; `--no-open` prints the URL without opening a
97
- browser.
106
+ `--no-serve` writes a static read-only preview HTML instead (no write-back
107
+ **Save incidents** downloads the labels file); `--from` / `--to` bound the window;
108
+ `--no-open` prints the URL without opening a browser.
98
109
 
99
110
  ## `dtk test-alert <metric>`
100
111
 
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import click
17
17
 
18
+ from detectkit.autotune.labels import load_incidents_for_display, newest_labels_file
18
19
  from detectkit.cli._output import echo_done, echo_error, echo_noop
19
20
  from detectkit.cli.commands.autotune import _load_project
20
21
  from detectkit.cli.commands.run import parse_date, select_metrics
@@ -56,6 +57,26 @@ def run_tune(
56
57
 
57
58
  metric_path, config = metrics[0]
58
59
  name = config.name
60
+ interval_seconds = config.get_interval().seconds
61
+
62
+ # Seed the synced labeler with the newest already-marked incidents from the
63
+ # shared store incidents/<metric>/ (the same files dtk autotune reads), so
64
+ # labeling round-trips across both tools. Best-effort — a missing/bad file
65
+ # just yields no seed.
66
+ incidents_dir = project_root / "incidents" / name
67
+ preload_incidents: list[dict[str, str]] = []
68
+ newest = newest_labels_file(incidents_dir)
69
+ if newest is not None:
70
+ try:
71
+ preload_incidents = load_incidents_for_display(
72
+ newest, interval_seconds=interval_seconds, metric_name=name
73
+ )
74
+ click.echo(
75
+ f" Seeded {len(preload_incidents)} incident(s) from "
76
+ f"{newest.relative_to(project_root)}"
77
+ )
78
+ except Exception as exc: # noqa: BLE001 — preload is a best-effort convenience
79
+ click.echo(click.style(f" Could not preload {newest}: {exc}", fg="yellow"))
59
80
 
60
81
  from_dt = parse_date(from_date) if from_date else None
61
82
  to_dt = parse_date(to_date) if to_date else None
@@ -68,6 +89,7 @@ def run_tune(
68
89
  start=from_dt,
69
90
  end=to_dt,
70
91
  project_name=project_name,
92
+ incidents=preload_incidents,
71
93
  )
72
94
  n_points = len(payload["points"])
73
95
  if n_points == 0:
@@ -97,6 +119,9 @@ def run_tune(
97
119
  project_root=project_root,
98
120
  open_browser=not no_open,
99
121
  echo=click.echo,
122
+ metric_name=name,
123
+ incidents_dir=incidents_dir,
124
+ interval_seconds=interval_seconds,
100
125
  )
101
126
  if applied is None:
102
127
  echo_noop(name, "tuning cancelled — metric unchanged")
@@ -0,0 +1,80 @@
1
+ "use strict";(()=>{var ye={"--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 L(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||ye[e]||"#888"}function we(e){let t=e.replace("#","").trim();t.length===3&&(t=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]);let n=parseInt(t,16);return t.length!==6||Number.isNaN(n)?[209,91,54]:[n>>16&255,n>>8&255,n&255]}function N(e,t){let[n,i,a]=we(e);return`rgba(${n},${i},${a},${t})`}function ee(e){let t=Math.max(1,window.devicePixelRatio||1),n=e.clientWidth||e.offsetWidth||0,i=e.clientHeight||e.offsetHeight||0;return e.width=Math.round(n*t),e.height=Math.round(i*t),t}function te(e,t,n,i){let a=()=>e.width-(t.l+t.r)*i,h=()=>e.height-(t.t+t.b)*i,r=()=>n.tmax-n.tmin||1,l=()=>n.vmax-n.vmin||1;return{px:m=>t.l*i+(m-n.tmin)/r()*a(),py:m=>e.height-t.b*i-(m-n.vmin)/l()*h(),tAt:m=>n.tmin+(m-t.l*i)/(a()||1)*r(),vAt:m=>n.vmin+(e.height-t.b*i-m)/(h()||1)*l(),plotW:a,plotH:h,tspan:r}}var F=Number.isFinite;function ne(e,t,n,i,a,h,r,l,p,f,b,u){let m=t.length,$=Math.max(1,Math.round(r)),C=a-i||1,T=0;for(let y=0;y<m;y++){let s=n[y];!F(s)||t[y]<i||t[y]>a||T++}if(e.strokeStyle=f,e.lineWidth=b*u,e.lineJoin="round",e.beginPath(),T<=$){let y=!1;for(let s=0;s<m;s++){let S=n[s],M=t[s];if(!F(S)||M<i||M>a){y=!1;continue}let E=l(M),R=p(S);y?e.lineTo(E,R):(e.moveTo(E,R),y=!0)}}else{let y=new Array($).fill(null),s=new Array($).fill(null);for(let M=0;M<m;M++){let E=n[M],R=t[M];if(!F(E)||R<i||R>a)continue;let A=Math.floor((R-i)/C*($-1));A=A<0?0:A>$-1?$-1:A,(y[A]===null||E<y[A])&&(y[A]=E),(s[A]===null||E>s[A])&&(s[A]=E)}let S=!1;for(let M=0;M<$;M++){if(s[M]===null){S=!1;continue}let E=h+M,R=p(s[M]),A=p(y[M]);S?e.lineTo(E,R):(e.moveTo(E,R),S=!0),e.lineTo(E,A)}}e.stroke()}function re(e){let t=[],n=-1;for(let i=0;i<e.length;i++){let a=e[i];a.lo!==null&&a.hi!==null&&F(a.lo)&&F(a.hi)?n===-1&&(n=i):n!==-1&&(t.push([n,i-1]),n=-1)}return n!==-1&&t.push([n,e.length-1]),t}function oe(e,t,n,i,a,h,r,l,p){e.fillStyle=N(h,r);for(let[f,b]of n){e.beginPath(),e.moveTo(i(t[f].t),a(t[f].hi));for(let u=f+1;u<=b;u++)e.lineTo(i(t[u].t),a(t[u].hi));for(let u=b;u>=f;u--)e.lineTo(i(t[u].t),a(t[u].lo));e.closePath(),e.fill()}e.strokeStyle=N(h,l),e.lineWidth=1*p;for(let[f,b]of n)for(let u of["hi","lo"]){e.beginPath();for(let m=f;m<=b;m++){let $=i(t[m].t),C=a(t[m][u]);m===f?e.moveTo($,C):e.lineTo($,C)}e.stroke()}}function ie(e,t,n,i,a,h,r,l){for(let p=0;p<t.length;p++){let f=t[p];if(!F(f.v)||f.t<n||f.t>i)continue;let b=a(f.t),u=h(f.v);e.fillStyle=N(r,.18),e.beginPath(),e.arc(b,u,6*l,0,Math.PI*2),e.fill(),e.fillStyle=r,e.beginPath(),e.arc(b,u,3*l,0,Math.PI*2),e.fill()}}function se(e,t,n,i,a,h,r,l,p,f,b){e.font=`${11*b}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let m=0;m<=4;m++){let $=i.vmin+(i.vmax-i.vmin)*m/4,C=h($);e.strokeStyle=N(p,.1),e.lineWidth=1*b,e.beginPath(),e.moveTo(n.l*b,C),e.lineTo(t.width-n.r*b,C),e.stroke(),e.fillStyle=f,e.textAlign="right",e.fillText(W($),(n.l-8)*b,C)}e.textBaseline="top";let u=l-r||1;for(let m=0;m<=5;m++){let $=r+u*m/5,C=a($);e.fillStyle=f,e.textAlign=m===0?"left":m===5?"right":"center",e.fillText($e($,u),C,(t.height-n.b+7)*b)}}function W(e){let t=Math.abs(e);return t>=1e3?e.toFixed(0):t>=10?e.toFixed(1):t>=1?e.toFixed(2):e.toFixed(3)}function $e(e,t){let n=new Date(e).toISOString();return t<2*864e5?n.slice(5,16).replace("T"," "):n.slice(5,10)}function X(e){return new Date(e).toISOString().slice(0,19).replace("T"," ")}function ae(e){let t=Math.round(e/6e4);if(t<60)return t+"m";let n=Math.floor(t/60),i=t%60;if(n<24)return n+"h"+(i?" "+i+"m":"");let a=Math.floor(n/24),h=n%24;return a+"d"+(h?" "+h+"h":"")}function le(e,t,n){return{left:t.l*n,top:t.t*n,right:e.width-t.r*n,bottom:e.height-t.b*n}}function ce(e,t,n,i,a,h,r){let l=le(t,n,i),p=Math.max(l.left,Math.min(a(h),l.right));p<=l.left+.5||(e.save(),e.fillStyle="rgba(17,15,13,0.42)",e.fillRect(l.left,l.top,p-l.left,l.bottom-l.top),e.strokeStyle=N(L("--faint"),.7),e.lineWidth=1*i,e.setLineDash([4*i,4*i]),e.beginPath(),e.moveTo(p,l.top),e.lineTo(p,l.bottom),e.stroke(),e.setLineDash([]),e.fillStyle=N(L("--faint"),.95),e.font=`${10*i}px ui-monospace, monospace`,e.textAlign="left",e.textBaseline="top",e.fillText(r,p+6*i,l.top+5*i),e.restore())}function de(e,t,n,i,a,h,r){let l=le(t,n,i),p=5*i;e.save();for(let f=0;f<h.length;f++){let b=h[f],u=a(b.t);if(u<l.left-1||u>l.right+1)continue;let m=r(b.kind);e.strokeStyle=N(m,.45),e.lineWidth=1*i,e.beginPath(),e.moveTo(u,l.top),e.lineTo(u,l.bottom),e.stroke(),e.fillStyle=m,e.beginPath(),e.moveTo(u-p,l.top),e.lineTo(u+p,l.top),e.lineTo(u,l.top+p*1.4),e.closePath(),e.fill()}e.restore()}var P={l:56,r:16,t:14,b:28},Me=300*1e3,d="dtk-report";function G(e){return e==="recovery"?L("--st-recovery"):e==="no_data"?L("--st-nodata"):L("--st-anomaly")}function Ae(e){return e==="recovery"?"recovery":e==="no_data"?"no-data":"anomaly"}var w=e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),K=(e,t,n)=>Math.max(t,Math.min(n,e)),ue=["--clay","--st-error","--st-recovery","--st-nodata"];function Te(e,t){Re(),t.classList.add(d),t.innerHTML="";let n=document.createElement("div");n.className="dtk-report-root",t.appendChild(n);let i=new Map;for(let s of e.points)s.v!==null&&i.set(s.t,s.v);let a=e.detectors.map((s,S)=>{let M=s.effective_start,E=s.points.map(A=>M!==null&&A.t<M?{t:A.t,lo:null,hi:null}:{t:A.t,lo:A.lo,hi:A.hi}),R=[];for(let A of s.points)if(A.a===1&&(M===null||A.t>=M)){let I=i.get(A.t);I!==void 0&&R.push({t:A.t,v:I})}return{det:s,band:E,anomalies:R,effectiveStart:M,color:L(ue[S%ue.length]),shown:S===0}});a.length===1&&(a[0].shown=!0),n.appendChild(Se(e));let h=null;a.length>1&&(h=Ce(a,()=>u.repaint()),n.appendChild(h));let r=document.createElement("div");r.className="dtk-bar",n.appendChild(r);let l=document.createElement("div");l.className="dtk-presets",r.appendChild(l);let p=document.createElement("div");p.className="dtk-readout",p.textContent="hover the chart for a point readout",r.appendChild(p);let f=document.createElement("div");f.className="dtk-chart";let b=document.createElement("canvas");f.appendChild(b),n.appendChild(f);let u=Pe(b,e,a,i,s=>{p.innerHTML=s}),m=[{label:"24h",ms:24*3600*1e3},{label:"7d",ms:7*86400*1e3},{label:"30d",ms:30*86400*1e3},{label:"All",ms:null}];for(let s of m){let S=document.createElement("button");S.className="dtk-preset",S.textContent=s.label,S.onclick=()=>{s.ms===null?u.resetView():u.setView(e.period.end-s.ms,e.period.end),Le(l,S)},l.appendChild(S)}let $=document.createElement("label");$.className="dtk-zero",$.title="Draw a horizontal line at y = 0 and scale the chart to include zero.";let C=document.createElement("input");C.type="checkbox",C.onchange=()=>u.setZeroLine(C.checked),$.appendChild(C),$.appendChild(document.createTextNode(" y = 0")),l.appendChild($),n.appendChild(Ee(e,s=>u.focusAlert(s))),u.resize();let T=0,y=()=>{T||(T=requestAnimationFrame(()=>{T=0,u.resize()}))};window.addEventListener("resize",y)}function Se(e){let t=document.createElement("div");t.className="dtk-header";let n=e.interval_seconds/60,i=e.interval_seconds>=86400?e.interval_seconds/86400+"d":e.interval_seconds>=3600?e.interval_seconds/3600+"h":n>=1?n+"min":e.interval_seconds+"s",a=e.project?`${w(e.project)} \xB7 ${w(e.metric)}`:w(e.metric),h=e.summary;return t.innerHTML=`<div class="dtk-h-top"><h1 class="dtk-title">${a}</h1><div class="dtk-meta">${w(X(e.period.start))} \u2013 ${w(X(e.period.end))} \xB7 interval ${w(i)}${e.generated_at?` \xB7 generated ${w(e.generated_at)}`:""}</div></div>`+(e.description?`<p class="dtk-desc">${w(e.description)}</p>`:"")+'<div class="dtk-chips">'+Y("anomalies",h.anomalies,"--st-anomaly")+Y("alerts",h.alerts,"--clay")+Y("recoveries",h.recoveries,"--st-recovery")+Y("no-data",h.no_data,"--st-nodata")+"</div>",t}function Y(e,t,n){let i=L(n);return`<span class="dtk-chip"><span class="dtk-dot" style="background:${w(i)}"></span><span class="dtk-chip-n">${t}</span><span class="dtk-chip-l">${w(e)}</span></span>`}function Ce(e,t){let n=document.createElement("div");return n.className="dtk-legend",e.forEach(i=>{let a=document.createElement("button");a.className="dtk-legend-item"+(i.shown?"":" off"),a.innerHTML=`<span class="dtk-swatch" style="background:${w(i.color)}"></span><span class="dtk-legend-name">${w(i.det.name)}</span><span class="dtk-legend-id">${w(i.det.id.slice(0,8))}</span><span class="dtk-legend-n">${i.det.anomaly_count}</span>`,a.onclick=()=>{i.shown=!i.shown,a.classList.toggle("off",!i.shown),t()},n.appendChild(a)}),n}function Ee(e,t){let n=document.createElement("div");n.className="dtk-alerts";let i=document.createElement("div");if(i.className="dtk-alerts-head",i.textContent=`Alerts (${e.alerts.length})`,n.appendChild(i),e.alerts.length===0){let r=document.createElement("div");return r.className="dtk-alerts-empty",r.textContent="No alerts fired in this period.",n.appendChild(r),n}let a=document.createElement("div");a.className="dtk-alerts-list";let h=[...e.alerts].sort((r,l)=>l.t-r.t);for(let r of h){let l=document.createElement("button");l.className="dtk-alert-row";let p=G(r.kind),f=r.direction!=="none"?` \xB7 ${w(r.direction)}`:"",b=r.severity>0?` \xB7 sev ${r.severity.toFixed(2)}`:"",u=r.value!==null?` \xB7 value ${W(r.value)}`:"",m=r.onset!==null&&r.kind!=="no_data"?` \xB7 ${ae(Math.max(0,r.t-r.onset))} (${r.consecutive} pts)`:"";l.innerHTML=`<span class="dtk-alert-time">${w(X(r.t))}</span><span class="dtk-badge" style="background:${w(N(p,.18))};color:${w(p)};border-color:${w(N(p,.5))}">${w(Ae(r.kind))}</span><span class="dtk-alert-body"><span class="dtk-alert-rule">${w(r.rule)}</span><span class="dtk-alert-sub">${w(r.detector)}${f}${b}${u}${w(m)}</span></span>`,l.onclick=()=>t(r),a.appendChild(l)}return n.appendChild(a),n}function Le(e,t){e.querySelectorAll(".dtk-preset").forEach(n=>n.classList.remove("active")),t.classList.add("active")}function Pe(e,t,n,i,a){let h=e.getContext("2d");if(!h)throw new Error("report: 2D context unavailable");let r=h,l=t.points.map(o=>o.t),p=t.points.map(o=>o.v===null?NaN:o.v),f=t.period.start,b=t.period.end,u=b-f||1,m=Math.min(Me,u),$=0,C=1;E();let T=f,y=b,s=1,S=null,M=!1;function E(){let o=1/0,c=-1/0;for(let k of p)Number.isFinite(k)&&(k<o&&(o=k),k>c&&(c=k));for(let k of n)for(let x of k.band)x.lo!==null&&Number.isFinite(x.lo)&&x.lo<o&&(o=x.lo),x.hi!==null&&Number.isFinite(x.hi)&&x.hi>c&&(c=x.hi);(!Number.isFinite(o)||!Number.isFinite(c))&&(o=0,c=1),M&&(o>0&&(o=0),c<0&&(c=0)),c<=o&&(c=o+1);let v=(c-o)*.06;$=o-v,C=c+v}function R(o){M!==o&&(M=o,E(),V())}function A(){for(let o of n)if(o.shown&&o.effectiveStart!==null)return o.effectiveStart;for(let o of n)if(o.effectiveStart!==null)return o.effectiveStart;return null}function I(){return{tmin:T,tmax:y,vmin:$,vmax:C}}function fe(){return te(e,P,I(),s)}function O(o,c){let v=c-o;if(v<m){let k=(o+c)/2;o=k-m/2,c=k+m/2,v=m}v>=u&&(o=f,c=b),o<f&&(c+=f-o,o=f),c>b&&(o-=c-b,c=b),T=K(o,f,b),y=K(c,f,b),V()}function Z(){T=f,y=b,V()}function pe(o){let c=o.onset!==null?Math.min(o.onset,o.t):o.t,v=o.t,x=Math.max(v-c,m)*1.5+m;O(c-x,v+x)}let j=0;function q(){j===0&&(j=requestAnimationFrame(V))}function V(){if(j=0,e.width===0||e.height===0)return;let o=fe(),c=L("--faint"),v=L("--muted"),k=L("--clay");if(r.fillStyle=L("--term-bg"),r.fillRect(0,0,e.width,e.height),l.length===0)return;if(se(r,e,P,I(),o.px,o.py,T,y,c,v,s),M&&$<=0&&C>=0){let g=o.py(0);r.strokeStyle=N(v,.6),r.lineWidth=1.25*s,r.beginPath(),r.moveTo(P.l*s,g),r.lineTo(e.width-P.r*s,g),r.stroke(),r.fillStyle=v,r.textAlign="right",r.textBaseline="middle",r.fillText("0",(P.l-8)*s,g)}r.save(),r.beginPath(),r.rect(P.l*s,P.t*s,o.plotW(),o.plotH()),r.clip();let x=P.t*s,z=o.plotH();for(let g of t.alerts){if(g.onset===null||g.kind==="no_data")continue;let B=Math.min(g.onset,g.t),U=Math.max(g.onset,g.t);if(U<T||B>y)continue;let ke=G(g.kind),Q=o.px(B),ge=o.px(U);r.fillStyle=N(ke,.08),r.fillRect(Q,x,Math.max(ge-Q,1*s),z)}for(let g of n){if(!g.shown)continue;let B=re(g.band);oe(r,g.band,B,o.px,o.py,g.color,.13,.4,s)}ne(r,l,p,T,y,P.l*s,o.plotW(),o.px,o.py,k,1.5,s);for(let g of n)g.shown&&ie(r,g.anomalies,T,y,o.px,o.py,L("--st-anomaly"),s);let D=A();D!==null&&D>T&&ce(r,e,P,s,o.px,D,"detection at full power \u2192");let H=t.alerts.map(g=>({t:g.t,kind:g.kind}));de(r,e,P,s,o.px,H,g=>G(g)),S!==null&&he(o,x,z,c),r.restore()}function be(o){let c=l;if(c.length===0)return-1;let v=0,k=c.length-1;for(;v<k;){let x=v+k>>1;c[x]<o?v=x+1:k=x}return v>0&&o-c[v-1]<c[v]-o&&(v-=1),v}function he(o,c,v,k){let x=be(S);if(x<0)return;let z=l[x];if(z<T||z>y){a("hover the chart for a point readout");return}let D=o.px(z);r.strokeStyle=N(k,.45),r.lineWidth=1*s,r.setLineDash([2*s,2*s]),r.beginPath(),r.moveTo(D,c),r.lineTo(D,c+v),r.stroke(),r.setLineDash([]);let H=p[x];if(Number.isFinite(H)){let g=o.py(H);r.fillStyle=L("--term-bg"),r.beginPath(),r.arc(D,g,4*s,0,Math.PI*2),r.fill(),r.strokeStyle=L("--clay"),r.lineWidth=2*s,r.beginPath(),r.arc(D,g,4*s,0,Math.PI*2),r.stroke()}ve(x)}function ve(o){let c=l[o],v=p[o],k=`<span class="dtk-ro-t">${w(X(c))}</span>`;k+=`<span class="dtk-ro-v">value ${Number.isFinite(v)?W(v):"\u2014"}</span>`;for(let x of n){if(!x.shown)continue;let z=x.band[o],D=x.det.points[o];if(z&&z.lo!==null&&z.hi!==null){let H=D&&D.a===1,g=H&&D.sev!==null?` sev ${D.sev.toFixed(2)}`:"",B=H?`<span class="dtk-ro-anom" style="color:${w(L("--st-anomaly"))}">anomaly${w(g)}</span>`:'<span class="dtk-ro-ok">ok</span>';k+=`<span class="dtk-ro-det"><span class="dtk-swatch" style="background:${w(x.color)}"></span>${w(x.det.name)}: [${W(z.lo)}, ${W(z.hi)}] ${B}</span>`}}a(k)}function J(o){let c=e.getBoundingClientRect(),v=(o-c.left-P.l)/(c.width-(P.l+P.r)||1);return T+K(v,0,1)*(y-T)}e.addEventListener("wheel",o=>{o.preventDefault();let c=J(o.clientX),v=y-T,k=K(v*Math.pow(1.0015,o.deltaY),m,u),x=(c-T)/(v||1);O(c-x*k,c-x*k+k)},{passive:!1});let _=null;e.addEventListener("mousedown",o=>{_={x:o.clientX,vMin:T,vMax:y},e.style.cursor="grabbing"}),window.addEventListener("mousemove",o=>{if(!_)return;let c=e.getBoundingClientRect(),v=(_.vMax-_.vMin)/(c.width-(P.l+P.r)||1),k=(o.clientX-_.x)*v;O(_.vMin-k,_.vMax-k)}),window.addEventListener("mouseup",()=>{_&&(_=null,e.style.cursor="crosshair")}),e.addEventListener("mousemove",o=>{_||(S=J(o.clientX),q())}),e.addEventListener("mouseleave",()=>{S!==null&&(S=null,a("hover the chart for a point readout"),q())}),e.addEventListener("dblclick",()=>Z()),e.style.cursor="crosshair";function xe(){s=ee(e),V()}return{repaint:()=>q(),resize:xe,setView:O,resetView:Z,focusAlert:pe,setZeroLine:R}}var me=!1;function Re(){if(me)return;me=!0;let e=`
2
+ .${d}{--term-bg:#211e1a;--term-border:#332f29;--term-text:#c9c2b4;
3
+ --clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;--surface:#fbf9f3;
4
+ --border:#e6e0d4;--muted:#6e675b;--faint:#9a9384;
5
+ --anom:#d63232;--rec:#36a64f;--nod:#f0ad4e;
6
+ --sans:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
7
+ --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
8
+ font-family:var(--sans);color:var(--ink);}
9
+ .${d} *{box-sizing:border-box;}
10
+ .${d} .dtk-report-root{max-width:1100px;margin:0 auto;padding:20px 18px 40px;}
11
+ /* --- header row ----------------------------------------------------------- */
12
+ .${d} .dtk-header{margin-bottom:16px;padding-left:12px;
13
+ border-left:3px solid var(--clay);}
14
+ .${d} .dtk-h-top{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 14px;}
15
+ .${d} .dtk-title{font-size:21px;font-weight:700;margin:0;color:var(--ink);
16
+ font-family:var(--sans);letter-spacing:-0.01em;}
17
+ .${d} .dtk-meta{font-size:12px;color:var(--muted);font-family:var(--mono);}
18
+ .${d} .dtk-desc{margin:8px 0 0;font-size:13px;color:var(--muted);max-width:760px;
19
+ line-height:1.5;}
20
+ /* --- summary chips (surface cards) ---------------------------------------- */
21
+ .${d} .dtk-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
22
+ .${d} .dtk-chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;
23
+ background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:12px;}
24
+ .${d} .dtk-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
25
+ .${d} .dtk-chip-n{font-weight:700;font-family:var(--mono);color:var(--ink);}
26
+ .${d} .dtk-chip-l{color:var(--faint);font-family:var(--mono);font-size:11px;
27
+ text-transform:uppercase;letter-spacing:0.05em;}
28
+ /* --- detector legend ------------------------------------------------------ */
29
+ .${d} .dtk-legend{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;}
30
+ .${d} .dtk-legend-item{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;
31
+ background:var(--surface);border:1px solid var(--border);border-radius:8px;cursor:pointer;
32
+ color:var(--ink);font-size:12px;font-family:var(--sans);transition:border-color .12s ease;}
33
+ .${d} .dtk-legend-item:hover{border-color:var(--clay);}
34
+ .${d} .dtk-legend-item.off{opacity:0.45;}
35
+ .${d} .dtk-legend-id{color:var(--faint);font-family:var(--mono);font-size:11px;}
36
+ .${d} .dtk-legend-n{color:var(--anom);font-weight:700;font-family:var(--mono);}
37
+ .${d} .dtk-swatch{width:10px;height:10px;border-radius:2px;display:inline-block;}
38
+ /* --- toolbar (presets + readout) ------------------------------------------ */
39
+ .${d} .dtk-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
40
+ gap:8px;margin-bottom:8px;}
41
+ .${d} .dtk-presets{display:flex;gap:5px;}
42
+ .${d} .dtk-preset{padding:5px 13px;background:var(--surface);
43
+ border:1px solid var(--border);border-radius:8px;color:var(--muted);cursor:pointer;
44
+ font-size:12px;font-family:var(--sans);transition:border-color .12s ease,color .12s ease;}
45
+ .${d} .dtk-preset:hover{border-color:var(--clay);color:var(--ink);}
46
+ .${d} .dtk-preset.active{background:var(--clay);color:#fff;border-color:var(--clay);}
47
+ .${d} .dtk-zero{display:inline-flex;align-items:center;gap:5px;margin-left:6px;
48
+ font-size:12px;color:var(--muted);font-family:var(--sans);cursor:pointer;user-select:none;}
49
+ .${d} .dtk-zero input{accent-color:var(--clay);cursor:pointer;}
50
+ .${d} .dtk-readout{font-size:11px;color:var(--muted);
51
+ font-family:var(--mono);display:flex;flex-wrap:wrap;gap:4px 12px;align-items:center;}
52
+ .${d} .dtk-readout .dtk-swatch{margin-right:4px;}
53
+ .${d} .dtk-ro-t{font-weight:700;color:var(--ink);}
54
+ /* --- chart panel (dark terminal surface) ---------------------------------- */
55
+ .${d} .dtk-chart{position:relative;width:100%;height:360px;background:var(--term-bg);
56
+ border:1px solid var(--term-border);border-radius:12px;overflow:hidden;
57
+ box-shadow:0 24px 60px -30px rgba(27,25,22,.45);}
58
+ .${d} .dtk-chart canvas{width:100%;height:100%;display:block;}
59
+ /* --- alerts list (surface cards) ------------------------------------------ */
60
+ .${d} .dtk-alerts{margin-top:18px;}
61
+ .${d} .dtk-alerts-head{font-size:12px;font-weight:600;color:var(--faint);
62
+ margin-bottom:9px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.06em;}
63
+ .${d} .dtk-alerts-empty{font-size:13px;color:var(--muted);padding:8px 0;}
64
+ .${d} .dtk-alerts-list{display:flex;flex-direction:column;gap:5px;
65
+ max-height:340px;overflow:auto;}
66
+ .${d} .dtk-alert-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
67
+ padding:8px 11px;background:var(--surface);border:1px solid var(--border);
68
+ border-radius:8px;cursor:pointer;color:var(--ink);font-family:var(--sans);
69
+ transition:border-color .12s ease,box-shadow .12s ease;}
70
+ .${d} .dtk-alert-row:hover{border-color:var(--clay);
71
+ box-shadow:0 4px 14px -8px rgba(27,25,22,.35);}
72
+ .${d} .dtk-alert-time{font-size:11px;color:var(--muted);
73
+ font-family:var(--mono);white-space:nowrap;min-width:142px;}
74
+ .${d} .dtk-badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:11px;
75
+ font-weight:700;border:1px solid;text-transform:uppercase;letter-spacing:0.03em;white-space:nowrap;}
76
+ .${d} .dtk-alert-body{display:flex;flex-direction:column;gap:2px;min-width:0;}
77
+ .${d} .dtk-alert-rule{font-size:12px;color:var(--ink);
78
+ font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
79
+ .${d} .dtk-alert-sub{font-size:11px;color:var(--muted);}
80
+ `,t=document.createElement("style");t.setAttribute("data-dtk-report",""),t.textContent=e,document.head.appendChild(t)}window.__DTK_REPORT__={render:Te};})();
@@ -0,0 +1,117 @@
1
+ "use strict";(()=>{var Vt={"--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 G(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||Vt[t]||"#888"}function Ut(t){let i=t.replace("#","").trim();i.length===3&&(i=i[0]+i[0]+i[1]+i[1]+i[2]+i[2]);let m=parseInt(i,16);return i.length!==6||Number.isNaN(m)?[209,91,54]:[m>>16&255,m>>8&255,m&255]}function R(t,i){let[m,e,v]=Ut(t);return`rgba(${m},${e},${v},${i})`}function Wt(t){let i=Math.max(1,window.devicePixelRatio||1),m=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(m*i),t.height=Math.round(e*i),i}var kt=Number.isFinite;function St(t,i,m,e,v,A,I,g,n,y,p,E){let F=i.length,D=Math.max(1,Math.round(I)),q=v-e||1,de=0;for(let M=0;M<F;M++){let Y=m[M];!kt(Y)||i[M]<e||i[M]>v||de++}if(t.strokeStyle=y,t.lineWidth=p*E,t.lineJoin="round",t.beginPath(),de<=D){let M=!1;for(let Y=0;Y<F;Y++){let L=m[Y],C=i[Y];if(!kt(L)||C<e||C>v){M=!1;continue}let j=g(C),Q=n(L);M?t.lineTo(j,Q):(t.moveTo(j,Q),M=!0)}}else{let M=new Array(D).fill(null),Y=new Array(D).fill(null);for(let C=0;C<F;C++){let j=m[C],Q=i[C];if(!kt(j)||Q<e||Q>v)continue;let W=Math.floor((Q-e)/q*(D-1));W=W<0?0:W>D-1?D-1:W,(M[W]===null||j<M[W])&&(M[W]=j),(Y[W]===null||j>Y[W])&&(Y[W]=j)}let L=!1;for(let C=0;C<D;C++){if(Y[C]===null){L=!1;continue}let j=A+C,Q=n(Y[C]),W=n(M[C]);L?t.lineTo(j,Q):(t.moveTo(j,Q),L=!0),t.lineTo(j,W)}}t.stroke()}function $t(t){let i=Math.abs(t);return i>=1e3?t.toFixed(0):i>=10?t.toFixed(1):i>=1?t.toFixed(2):t.toFixed(3)}function It(t,i){let m=new Date(t).toISOString();return i<2*864e5?m.slice(5,16).replace("T"," "):m.slice(5,10)}function Rt(t,i,m){return{left:i.l*m,top:i.t*m,right:t.width-i.r*m,bottom:t.height-i.b*m}}function zt(t,i,m,e,v,A,I){let g=Rt(i,m,e),n=Math.max(g.left,Math.min(v(A),g.right));n<=g.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(g.left,g.top,n-g.left,g.bottom-g.top),t.strokeStyle=R(G("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(n,g.top),t.lineTo(n,g.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=R(G("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(I,n+6*e,g.top+5*e),t.restore())}function Ht(t,i,m,e,v,A,I){let g=Rt(i,m,e),n=5*e;t.save();for(let y=0;y<A.length;y++){let p=A[y],E=v(p.t);if(E<g.left-1||E>g.right+1)continue;let F=I(p.kind);t.strokeStyle=R(F,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(E,g.top),t.lineTo(E,g.bottom),t.stroke(),t.fillStyle=F,t.beginPath(),t.moveTo(E-n,g.top),t.lineTo(E+n,g.top),t.lineTo(E,g.top+n*1.4),t.closePath(),t.fill()}t.restore()}var Gt={mad:1,zscore:2,iqr:4};function Yt(t,i){let m=t.seasonalityData;if(!m||m.length===0)return 0;let e=0;for(let v of i){let A=new Set;for(let I of m)A.add(v.map(g=>{var n;return String((n=I==null?void 0:I[g])!=null?n:"")}).join("|"));e=Math.max(e,A.size)}return e}function Bt(t,i){let m=t.timestamps.length;if(i.type==="manual_bounds")return Math.min(i.inputType!=="values"?1:0,m);let e=Math.max(i.minSamples,Gt[i.type]);if(i.smoothing==="sma"&&(e=Math.max(e,i.smoothingWindow-1)),i.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/i.smoothingAlpha))),i.inputType!=="values"&&(e=Math.max(e,1)),i.seasonalityComponents!==null&&i.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let A=Yt(t,i.seasonalityComponents);if(A>0){let I=i.minSamplesPerGroup*A;i.windowSize>=I&&(e=Math.max(e,I))}}return Math.min(e,m)}var N={l:52,r:14,t:14,b:26},Mt=46,Kt=10,Xt=13,ce=Number.isFinite,We=1e3,$e=60*We,Ie=60*$e,re=24*Ie,Ct=[We,2*We,5*We,10*We,15*We,30*We,$e,2*$e,5*$e,10*$e,15*$e,30*$e,Ie,2*Ie,3*Ie,6*Ie,12*Ie,re,2*re,3*re,5*re,7*re,14*re];function Ot(t,i,m){let e=Math.max(i-t,1);if(e>m*14*re){let g=[1,2,3,6,12,24,36,60,120,240],n=g[g.length-1];for(let q of g)if(e/(q*30.44*re)<=m){n=q;break}let y=new Date(t),p=y.getUTCFullYear(),E=y.getUTCMonth();if(n>=12){let q=n/12;p=Math.floor(p/q)*q,E=0}else E=Math.floor(E/n)*n;let F=[],D=Date.UTC(p,E,1);for(;D<=i;)D>=t&&F.push(D),E+=n,p+=Math.floor(E/12),E%=12,D=Date.UTC(p,E,1);return{ticks:F,step:n*30*re}}let v=Ct[Ct.length-1];for(let g of Ct)if(e/g<=m){v=g;break}let A;if(v%re===0){let g=new Date(t);A=Date.UTC(g.getUTCFullYear(),g.getUTCMonth(),g.getUTCDate());let n=v/re,y=Math.round(A/re);A+=(n-y%n)%n*re}else A=Math.ceil(t/v)*v;let I=[];for(let g=A;g<=i;g+=v)g>=t&&I.push(g);return{ticks:I,step:v}}function Tt(t,i){let m=new Date(t).toISOString();return i>=320*re?m.slice(0,4):i>=26*re?m.slice(0,7):i>=re?m.slice(5,10):i>=Ie?m.slice(5,16).replace("T"," "):m.slice(5,19).replace("T"," ")}function At(t,i={}){let m=t.getContext("2d");if(!m)throw new Error("chart: 2D context unavailable");let e=m,v=!!i.navigable,A=v&&i.showNavigator!==!1,I=i.yFit==="data",g=!!i.labeling,n=1,y=null,p=-1,E=0,F=!!i.showZeroLine,D=[],q=null,de=-1,M=null,Y=!1,L=0,C=1,j=0,Q=1,W=0,U=1,ct="";function dt(){n=Wt(t)}let De=()=>A?(Kt+Mt)*n:0,le=()=>t.width-(N.l+N.r)*n,we=()=>t.height-(N.t+N.b)*n-De(),ke=()=>v?W:L,Ke=()=>v?U:C,ue=()=>Ke()-ke()||1,fe=()=>C-L||1,T=o=>N.l*n+(o-ke())/ue()*le(),O=o=>t.height-N.b*n-De()-(o-j)/(Q-j||1)*we(),be=()=>t.height-Mt*n,ie=()=>(Mt-Xt)*n,Le=()=>t.width-(N.l+N.r)*n,K=o=>N.l*n+(o-L)/fe()*Le(),Xe=o=>be()+ie()-(o-j)/(Q-j||1)*ie(),Ze=(o,s,u)=>{let c=new Array(o.length).fill(!1);if(o.length===0)return c;let a=e.measureText(Tt(o[0],u)).width+16*n,d=-1/0;for(let b=0;b<o.length;b++){let k=s(o[b]);k-d>=a&&(c[b]=!0,d=k)}return c},X=(o,s,u)=>Math.max(s,Math.min(u,o)),Se=()=>{let o=y==null?void 0:y.series,s=o?o.timestamps.length:0,u=s>1?fe()/(s-1):1e3;return Math.max(u*8,1e3)};function se(o,s){var a;let u=s-o,c=Se();if(u<c){let d=(o+s)/2;o=d-c/2,s=d+c/2,u=c}u>=fe()&&(o=L,s=C),o<L&&(s+=L-o,o=L),s>C&&(o-=s-C,s=C),W=X(o,L,C),U=X(s,L,C),Y||(a=i.onViewChange)==null||a.call(i,W,U),ee()}function Je(o,s){v&&(Y=!0,se(o,s),Y=!1)}let ge=o=>{let s=(o-N.l*n)/(le()||1);return ke()+X(s,0,1)*ue()},Re=o=>{let s=(o-N.l*n)/(Le()||1);return L+X(s,0,1)*fe()};function Qe(o){let s=y==null?void 0:y.series;if(!s||s.timestamps.length===0)return-1;let u=(o-N.l*n)/(le()||1),c=ke()+u*ue(),a=s.timestamps,d=0,b=a.length-1;for(;d<b;){let k=d+b>>1;a[k]<c?d=k+1:b=k}return d>0&&c-a[d-1]<a[d]-c&&(d-=1),d}function ze(o,s){let u=o.timestamps;if(L=u[0],C=u[u.length-1],v){let b=`${u.length}:${L}:${C}`;b!==ct?(ct=b,W=L,U=C):(W=X(W,L,C),U=X(Math.max(U,W+Se()),L,C))}let c=1/0,a=-1/0;for(let b of o.values)ce(b)&&(b<c&&(c=b),b>a&&(a=b));if(!I)for(let b of s)b.scored&&(ce(b.lower)&&b.lower<c&&(c=b.lower),ce(b.upper)&&b.upper>a&&(a=b.upper));(!ce(c)||!ce(a))&&(c=0,a=1),F&&(c>0&&(c=0),a<0&&(a=0)),a<=c&&(a=c+1);let d=(a-c)*.06;j=c-d,Q=a+d}function He(o,s,u){let c=y.series.timestamps;St(e,c,o,ke(),Ke(),N.l*n,le(),T,O,s,u,n)}function xt(o,s){let u=[],c=-1;for(let a=Math.max(0,s);a<o.length;a++){let d=o[a];d.scored&&ce(d.lower)&&ce(d.upper)?c===-1&&(c=a):c!==-1&&(u.push([c,a-1]),c=-1)}return c!==-1&&u.push([c,o.length-1]),u}function et(){if(E=0,!y||t.width===0||t.height===0)return;let{series:o,scored:s,params:u,alerts:c}=y;if(o.timestamps.length===0){e.fillStyle=G("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let a=G("--clay"),d=G("--st-anomaly"),b=G("--faint"),k=G("--muted");e.fillStyle=G("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*n}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let x=0;x<=4;x++){let S=j+(Q-j)*x/4,w=O(S);e.strokeStyle=R(b,.1),e.lineWidth=1*n,e.beginPath(),e.moveTo(N.l*n,w),e.lineTo(t.width-N.r*n,w),e.stroke(),e.fillStyle=k,e.textAlign="right",e.fillText($t(S),(N.l-8)*n,w)}if(F&&j<=0&&Q>=0){let x=O(0);e.strokeStyle=R(k,.6),e.lineWidth=1.25*n,e.beginPath(),e.moveTo(N.l*n,x),e.lineTo(t.width-N.r*n,x),e.stroke(),e.fillStyle=k,e.textAlign="right",e.fillText("0",(N.l-8)*n,x)}e.textBaseline="top";let z=t.height-N.b*n-De();if(v){let x=Ot(ke(),Ke(),7),S=Ze(x.ticks,T,x.step);for(let w=0;w<x.ticks.length;w++){let $=x.ticks[w],oe=T($);e.strokeStyle=R(b,.1),e.lineWidth=1*n,e.beginPath(),e.moveTo(oe,N.t*n),e.lineTo(oe,z),e.stroke(),S[w]&&(e.fillStyle=k,e.textAlign=oe<(N.l+24)*n?"left":oe>t.width-(N.r+24)*n?"right":"center",e.fillText(Tt($,x.step),oe,z+7*n))}}else{let x=ue();for(let S=0;S<=5;S++){let w=L+x*S/5,$=T(w);e.fillStyle=k,e.textAlign=S===0?"left":S===5?"right":"center",e.fillText(It(w,x),$,z+7*n)}}e.save(),e.beginPath(),e.rect(N.l*n,N.t*n,le(),we()),e.clip(),yt();let H=s.length,B=Math.min(Bt(o,u),H),Z=B<H?o.timestamps[B]:void 0,J=xt(s,B);e.fillStyle=R(a,.13);for(let[x,S]of J){e.beginPath(),e.moveTo(T(s[x].timestamp),O(s[x].upper));for(let w=x+1;w<=S;w++)e.lineTo(T(s[w].timestamp),O(s[w].upper));for(let w=S;w>=x;w--)e.lineTo(T(s[w].timestamp),O(s[w].lower));e.closePath(),e.fill()}e.strokeStyle=R(a,.4),e.lineWidth=1*n;for(let[x,S]of J)for(let w of["upper","lower"]){e.beginPath();for(let $=x;$<=S;$++){let oe=T(s[$].timestamp),_e=O(s[$][w]);$===x?e.moveTo(oe,_e):e.lineTo(oe,_e)}e.stroke()}e.strokeStyle=R(b,.55),e.lineWidth=1*n,e.setLineDash([3*n,3*n]);for(let[x,S]of J){e.beginPath();for(let w=x;w<=S;w++){let $=s[w].center;if(!ce($))continue;let oe=T(s[w].timestamp),_e=O($);w===x?e.moveTo(oe,_e):e.lineTo(oe,_e)}e.stroke()}if(e.setLineDash([]),u.smoothing!=="none"){He(o.values,R(a,.28),1.25);let x=s.map(S=>S.processedValue);He(x,a,1.6)}else He(o.values,a,1.5);for(let x=B;x<H;x++){let S=s[x];if(!S.scored||!ce(S.value))continue;let w=T(S.timestamp),$=O(S.value);S.isAnomaly?(e.fillStyle=R(d,.18),e.beginPath(),e.arc(w,$,6*n,0,Math.PI*2),e.fill(),e.fillStyle=d,e.beginPath(),e.arc(w,$,3*n,0,Math.PI*2),e.fill()):o.truthAnomaly[x]&&(e.strokeStyle=R(k,.7),e.lineWidth=1.25*n,e.beginPath(),e.arc(w,$,3.5*n,0,Math.PI*2),e.stroke())}Z!==void 0&&zt(e,t,N,n,T,Z,"detection at full power \u2192"),p>=0&&p<s.length&&Oe(p,u.windowSize,s,o,b),c&&c.length&&Ht(e,t,N,n,T,c,x=>x==="anomaly"?G("--st-anomaly"):x==="recovery"?G("--st-recovery"):G("--st-nodata")),e.restore(),A&&Be(o,c,a,b,k)}function Be(o,s,u,c,a){let d=be(),b=d+ie(),k=N.l*n,z=t.width-N.r*n,H=Ot(L,C,5);e.save(),e.beginPath(),e.rect(k,d,Le(),ie()),e.clip(),e.strokeStyle=R(c,.1),e.lineWidth=1*n;for(let S of H.ticks){let w=K(S);e.beginPath(),e.moveTo(w,d),e.lineTo(w,b),e.stroke()}St(e,o.timestamps,o.values,L,C,k,Le(),K,Xe,R(u,.7),1.1,n),e.restore();let B=K(W),Z=K(U);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(k,d,B-k,ie()),e.fillRect(Z,d,z-Z,ie()),D.length){let S=G("--st-anomaly");e.fillStyle=R(S,.28);for(let w of D){let $=K(w.start),oe=Math.max(K(w.end)-$,2*n);e.fillRect($,d,oe,ie())}}if(s&&s.length){let S=G("--st-anomaly");e.fillStyle=R(S,.85);for(let w of s){let $=K(w.t);e.fillRect($-1*n,d,2*n,ie())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(B,d,Z-B,ie()),e.strokeStyle=u,e.lineWidth=1.5*n,e.strokeRect(B,d+1,Z-B,ie()-2),e.fillStyle=u;let J=d+ie()/2-8*n;e.fillRect(B-2*n,J,4*n,16*n),e.fillRect(Z-2*n,J,4*n,16*n),e.font=`${10*n}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=a;let x=Ze(H.ticks,K,H.step);for(let S=0;S<H.ticks.length;S++){let w=H.ticks[S],$=K(w);e.strokeStyle=R(c,.25),e.lineWidth=1*n,e.beginPath(),e.moveTo($,b),e.lineTo($,b+3*n),e.stroke(),x[S]&&(e.textAlign=$<(N.l+26)*n?"left":$>t.width-(N.r+26)*n?"right":"center",e.fillText(Tt(w,H.step),$,b+5*n))}}function Oe(o,s,u,c,a){let d=c.timestamps,b=Math.max(0,o-s),k=o-1,z=N.t*n,H=we();if(k>=b){let J=c.intervalSeconds*1e3/ue()*le()*.5,x=T(d[b])-J,S=T(d[k])+J;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(x,z,S-x,H),e.strokeStyle=R(a,.5),e.lineWidth=1*n,e.beginPath(),e.moveTo(x,z),e.lineTo(x,z+H),e.moveTo(S,z),e.lineTo(S,z+H),e.stroke()}let B=T(d[o]);e.strokeStyle=R(a,.85),e.lineWidth=1*n,e.setLineDash([2*n,2*n]),e.beginPath(),e.moveTo(B,z),e.lineTo(B,z+H),e.stroke(),e.setLineDash([]);let Z=u[o];if(Z.scored&&ce(Z.lower)&&ce(Z.upper)){let J=5*n;e.strokeStyle=R(G("--clay"),.85),e.lineWidth=1.5*n;for(let x of[Z.lower,Z.upper]){let S=O(x);e.beginPath(),e.moveTo(B-J,S),e.lineTo(B+J,S),e.stroke()}}if(ce(Z.value)){let J=O(Z.value);e.fillStyle=G("--term-bg"),e.beginPath(),e.arc(B,J,4*n,0,Math.PI*2),e.fill(),e.strokeStyle=Z.isAnomaly?G("--st-anomaly"):G("--clay"),e.lineWidth=2*n,e.beginPath(),e.arc(B,J,4*n,0,Math.PI*2),e.stroke()}}function he(o,s,u,c,a){e.beginPath(),e.moveTo(o+a,s),e.arcTo(o+u,s,o+u,s+c,a),e.arcTo(o+u,s+c,o,s+c,a),e.arcTo(o,s+c,o,s,a),e.arcTo(o,s,o+u,s,a),e.closePath()}function ve(o,s){let u=G("--st-anomaly"),c=14*n,a=3*n,d=o-c-a,b=N.t*n+a;e.fillStyle=s?R(u,.95):"rgba(27,25,22,0.82)",e.strokeStyle=R(u,.9),e.lineWidth=1*n,he(d,b,c,c,3*n),e.fill(),e.stroke(),e.strokeStyle=s?"#fff":u,e.lineWidth=1.5*n;let k=4*n;e.beginPath(),e.moveTo(d+k,b+k),e.lineTo(d+c-k,b+c-k),e.moveTo(d+c-k,b+k),e.lineTo(d+k,b+c-k),e.stroke()}function yt(){let o=N.t*n,s=we(),u=G("--st-anomaly"),c=N.l*n,a=t.width-N.r*n;for(let d=0;d<D.length;d++){let b=D[d],k=T(b.start),z=T(b.end);if(z<c-1||k>a+1)continue;let H=Math.max(z-k,2*n),B=g&&b===q;e.fillStyle=R(u,B?.3:.16),e.fillRect(k,o,H,s),e.strokeStyle=R(u,B?.95:.5),e.lineWidth=(B?2:1)*n,e.strokeRect(k,o,H,s),g&&(e.fillStyle=R(u,.95),e.fillRect(k-1.5*n,o,3*n,s),e.fillRect(z-1.5*n,o,3*n,s),(z-k>=22*n||B)&&ve(z,B||d===de))}if(g&&M&&M.mode==="new"){let d=T(M.a),b=T(M.b);e.fillStyle=R(G("--st-nodata"),.28),e.fillRect(Math.min(d,b),o,Math.abs(b-d),s)}}function tt(){var o;(o=i.onIncidentsChange)==null||o.call(i,D)}function Me(o){let s=D.indexOf(o);s<0||(D.splice(s,1),q===o&&(q=null),de=-1,tt(),ee())}function Fe(o,s){let u=N.t*n,c=6*n;for(let a=0;a<D.length;a++){let d=T(D[a].end);if(d-T(D[a].start)>=22*n||D[a]===q){let b=14*n,k=3*n,z=d-b-k,H=u+k;if(o>=z&&o<=z+b&&s>=H&&s<=H+b)return{i:a,edge:"del"}}}for(let a=0;a<D.length;a++){let d=T(D[a].start),b=T(D[a].end);if(Math.abs(o-d)<=c)return{i:a,edge:"a"};if(Math.abs(o-b)<=c)return{i:a,edge:"b"}}for(let a=0;a<D.length;a++){let d=T(D[a].start),b=T(D[a].end);if(o>d+c&&o<b-c)return{i:a,edge:"move"}}return null}let qe=()=>{let o=y==null?void 0:y.series,s=o?o.timestamps.length:0;return s>1?Math.max(fe()/(s-1),1):1e3};function ee(){E===0&&(E=requestAnimationFrame(et))}function Pe(){var u;if(!i.onHover||!y)return;if(p<0){i.onHover(null);return}let o=(u=y.scored[p])!=null?u:null,s={index:p,point:o,windowStart:Math.max(0,p-y.params.windowSize),windowEnd:p-1};i.onHover(s)}let ae=null,te=null,Ce=o=>{let s=t.getBoundingClientRect();return{x:(o.clientX-s.left)*n,y:(o.clientY-s.top)*n}},nt=o=>A&&o>=be(),ne=o=>{let s=K(W),u=K(U),c=8*n;return Math.abs(o-s)<=c?"l":Math.abs(o-u)<=c?"r":o>s&&o<u?"move":"out"};function Ne(o){if(!y)return;let{x:s,y:u}=Ce(o);if(ae||te||M)return;if(nt(u)){p!==-1&&(p=-1,Pe(),ee());let a=ne(s);t.style.cursor=a==="l"||a==="r"?"ew-resize":a==="move"?"grab":"pointer";return}if(g){p!==-1&&(p=-1,Pe());let a=Fe(s,u),d=a&&a.edge==="del"?a.i:-1;d!==de&&(de=d,ee()),t.style.cursor=a?a.edge==="del"?"pointer":a.edge==="move"?"grab":"ew-resize":"crosshair";return}v&&(t.style.cursor="grab");let c=Qe(s);c!==p&&(p=c,Pe(),ee())}function je(){p!==-1&&(p=-1,Pe(),ee())}function Ve(o){if(!v||!y)return;let{x:s,y:u}=Ce(o);if(A&&u>=be()){let c=ne(s);if(c==="l")te={type:"l",grab:0,vMin:W,vMax:U};else if(c==="r")te={type:"r",grab:0,vMin:W,vMax:U};else if(c==="move")te={type:"move",grab:Re(s),vMin:W,vMax:U};else{let a=Re(s),d=ue();se(a-d/2,a+d/2),te={type:"move",grab:a,vMin:W,vMax:U}}t.style.cursor="grabbing",o.preventDefault();return}if(u>N.t*n&&u<t.height-N.b*n-De()){if(g){let c=ge(s),a=Fe(s,u);if(a&&a.edge==="del"){Me(D[a.i]),o.preventDefault();return}if(a&&a.edge==="move"){let d=D[a.i];q=d,M={mode:"move",iv:d,grab:c,a0:d.start,b0:d.end,moved:!1}}else if(a){let d=D[a.i];q=d,M={mode:"edge",iv:d,edge:a.edge,moved:!1}}else q=null,M={mode:"new",a:c,b:c,moved:!1};t.style.cursor="crosshair",ee(),o.preventDefault();return}ae={x:s,vMin:W,vMax:U},t.style.cursor="grabbing",o.preventDefault()}}function Te(o){if(!y)return;let{x:s}=Ce(o);if(M){let u=ge(s);if(M.mode==="new")M.b=u,Math.abs(T(M.b)-T(M.a))>4*n&&(M.moved=!0);else if(M.mode==="edge"){let c=M.iv,a=qe();M.edge==="a"?c.start=X(Math.min(u,c.end-a),L,C):c.end=X(Math.max(u,c.start+a),L,C),M.moved=!0}else{let c=M.iv,a=M.a0+(u-M.grab),d=M.b0+(u-M.grab);a<L&&(d+=L-a,a=L),d>C&&(a-=d-C,d=C),c.start=X(a,L,C),c.end=X(d,L,C),M.moved=!0}ee();return}if(ae){let u=(s-ae.x)*(ae.vMax-ae.vMin)/(le()||1);se(ae.vMin-u,ae.vMax-u)}else if(te){let u=Re(s);te.type==="l"?se(Math.min(u,U-Se()),U):te.type==="r"?se(W,Math.max(u,W+Se())):se(te.vMin+(u-te.grab),te.vMax+(u-te.grab))}}function ot(){if(M){let o=M;if(o.mode==="new")if(o.moved){let s=X(Math.min(o.a,o.b),L,C),u=X(Math.max(o.a,o.b),L,C),c={start:s,end:u,label:""};D.push(c),q=c}else q=null;else if(o.iv.start>o.iv.end){let s=o.iv.start;o.iv.start=o.iv.end,o.iv.end=s}M=null,t.style.cursor="crosshair",o.moved&&tt(),ee();return}(ae||te)&&(ae=null,te=null,t.style.cursor="grab")}function rt(o){let s=o.target;s&&(s.tagName==="INPUT"||s.tagName==="TEXTAREA"||s.isContentEditable)||!q||(o.key==="Delete"||o.key==="Backspace"?(o.preventDefault(),Me(q)):o.key==="Escape"&&(q=null,ee()))}function ut(o){if(!v||!y)return;o.preventDefault();let{x:s,y:u}=Ce(o),c=X(ue()*Math.pow(1.0015,o.deltaY),Se(),fe());if(u>=be()){let a=Re(s);se(a-c/2,a+c/2)}else{let a=ge(s),d=(a-W)/(ue()||1);se(a-d*c,a-d*c+c)}}function mt(){v&&se(L,C)}t.addEventListener("mousemove",Ne),t.addEventListener("mouseleave",je),v&&(t.addEventListener("mousedown",Ve),t.addEventListener("wheel",ut,{passive:!1}),t.addEventListener("dblclick",mt),window.addEventListener("mousemove",Te),window.addEventListener("mouseup",ot)),g&&(window.addEventListener("keydown",rt),t.style.cursor="crosshair");function pt(o){y=o,!g&&o.incidents&&(D=o.incidents),ze(o.series,o.scored),p>=o.series.timestamps.length&&(p=-1),ee()}function it(o){F!==o&&(F=o,y&&ze(y.series,y.scored),ee())}function Ee(o){D=o,q=null,de=-1,ee()}function ft(){dt(),y&&(ze(y.series,y.scored),ee())}function st(){t.removeEventListener("mousemove",Ne),t.removeEventListener("mouseleave",je),v&&(t.removeEventListener("mousedown",Ve),t.removeEventListener("wheel",ut),t.removeEventListener("dblclick",mt),window.removeEventListener("mousemove",Te),window.removeEventListener("mouseup",ot)),g&&window.removeEventListener("keydown",rt),E!==0&&cancelAnimationFrame(E),E=0,y=null}return dt(),{render:pt,resize:ft,setZeroLine:it,setViewWindow:Je,setIncidents:Ee,destroy:st}}var Zt={mad:3,zscore:3,iqr:1.5},Jt={mad:10,zscore:3,iqr:4},Qt="dtk-tune";function h(t,i,m){let e=document.createElement(t);return i&&(e.className=i),m!=null&&(e.textContent=m),e}function Ye(t,i){let m=h("label","dtk-ctl-label",t);if(i){m.title=i;let e=h("span","dtk-ctl-info","\u24D8");e.title=i,m.appendChild(document.createTextNode(" ")),m.appendChild(e)}return m}function at(t,i,m,e,v){let A=h("div","dtk-ctl");A.appendChild(Ye(t,v));let I=h("div","dtk-seg"),g=m,n=[],y=()=>{n.forEach(p=>p.classList.toggle("on",p.dataset.v===g))};return i.forEach(p=>{let E=h("button","dtk-seg-btn",p.label);E.type="button",E.dataset.v=p.value,E.onclick=()=>{g=p.value,y(),e(g)},n.push(E),I.appendChild(E)}),y(),A.appendChild(I),{row:A,get:()=>g,set:p=>{g=p,y()}}}function Ge(t,i,m){var y;let e=h("div","dtk-ctl"),v=h("div","dtk-ctl-head"),A=Ye(t,i.hint),I=h("span","dtk-ctl-val"),g=(y=i.fmt)!=null?y:(p=>String(p));v.appendChild(A),v.appendChild(I),e.appendChild(v);let n=h("input","dtk-range");return n.type="range",n.min=String(i.min),n.max=String(i.max),n.step=String(i.step),n.value=String(i.value),I.textContent=g(i.value),n.oninput=()=>{let p=Number(n.value);I.textContent=g(p),m(p)},e.appendChild(n),{row:e,get:()=>Number(n.value),setMax:p=>{n.max=String(p),Number(n.value)>p&&(n.value=String(p),I.textContent=g(p))}}}function qt(t){if(t.type==="manual_bounds"){let m={};return t.lowerBound!=null&&(m.lower_bound=t.lowerBound),t.upperBound!=null&&(m.upper_bound=t.upperBound),t.inputType!=="values"&&(m.input_type=t.inputType),m}let i={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(i.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(i.half_life=t.halfLife)),t.detrend!=="none"&&(i.detrend=t.detrend),t.smoothing!=="none"&&(i.smoothing=t.smoothing),t.inputType!=="values"&&(i.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(i.seasonality_components=t.seasonalityComponents,i.min_samples_per_group=t.minSamplesPerGroup),i}function en(t,i){let m=qt(t),e=[`type: ${t.type}`];for(let[v,A]of Object.entries(m))e.push(`${v}=${typeof A=="object"?JSON.stringify(A):A}`);return t.direction&&t.direction!=="any"&&e.push(`direction=${t.direction}`),e.push(`consecutive_anomalies=${i}`),e.join(" \xB7 ")}function tn(t,i){var Pt,Nt,Et;on(),i.classList.add(Qt),i.innerHTML="";let m=h("div","dtk-tune-root");i.appendChild(m);let e=t.points.length,v={timestamps:t.points.map(r=>r.t),values:t.points.map(r=>r.v==null?NaN:r.v),intervalSeconds:t.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:t.seasonality_columns.length?t.seasonality:void 0,seasonalityColumns:t.seasonality_columns.length?t.seasonality_columns:void 0},A=v,I=r=>{let l=Math.max(0,e-r);return l<=0?v:{timestamps:v.timestamps.slice(l),values:v.values.slice(l),intervalSeconds:v.intervalSeconds,truthAnomaly:v.truthAnomaly.slice(l),seasonalityData:v.seasonalityData?v.seasonalityData.slice(l):void 0,seasonalityColumns:v.seasonalityColumns}},g=r=>Date.parse(r.replace(" ","T")+"Z"),n=(t.incidents||[]).map(r=>({start:g(r.start),end:g(r.end),label:r.label||""})).filter(r=>Number.isFinite(r.start)&&Number.isFinite(r.end)).map(r=>({start:Math.min(r.start,r.end),end:Math.max(r.start,r.end),label:r.label})),y=[],p=t.detector,E=t.consecutive_anomalies,F=v.values.filter(r=>Number.isFinite(r)).sort((r,l)=>r-l),D=F.length?F[0]:0,q=F.length?F[F.length-1]:1,de=r=>F.length?F[Math.min(F.length-1,Math.max(0,Math.round(r*(F.length-1))))]:0,M=Math.max((q-D)*.05,1e-9),Y=D-M,L=q+M,C=Math.max((L-Y)/400,1e-9),j=p.lowerBound!=null?p.lowerBound:de(.05),Q=p.upperBound!=null?p.upperBound:de(.95),W=(Pt=p.seasonalityComponents)!=null?Pt:[],U=new Map;W.forEach((r,l)=>r.forEach(f=>U.set(f,l+1)));let ct=()=>{let r=0;U.forEach(f=>{f>r&&(r=f)});let l=[];for(let f=1;f<=r;f++){let P=t.seasonality_columns.filter(_=>U.get(_)===f);P.length&&l.push(P)}return l.length?l:null},dt=r=>{if(!r||!t.seasonality.length)return 0;let l=0;for(let f of r){let P=new Set;for(let _ of t.seasonality)P.add(f.map(V=>{var me;return String((me=_==null?void 0:_[V])!=null?me:"")}).join("|"));l=Math.max(l,P.size)}return l},De=()=>{var r;return{type:Ne.get(),threshold:Te.get(),windowSize:it.get(),minSamples:p.minSamples,inputType:p.inputType,smoothing:s.get(),smoothingAlpha:p.smoothingAlpha,smoothingWindow:p.smoothingWindow,windowWeights:Ee.get(),halfLife:Ee.get()==="exponential"?ft.get():null,detrend:o.get(),seasonalityComponents:ct(),minSamplesPerGroup:(r=Jt[Ne.get()])!=null?r:p.minSamplesPerGroup,consecutiveAnomalies:E,direction:c.get(),lowerBound:je.get(),upperBound:Ve.get()}},le=h("div","dtk-tune-header"),we=h("div","dtk-tune-titlerow");we.appendChild(h("h1","dtk-tune-title",t.metric));let ke=h("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");we.appendChild(ke),le.appendChild(we);let Ke=t.project?`${t.project} \xB7 `:"";le.appendChild(h("div","dtk-tune-sub",`${Ke}${e} points \xB7 ${nn(t.interval_seconds)} grid`)),t.description&&le.appendChild(h("div","dtk-tune-desc",t.description)),m.appendChild(le);let ue=h("div","dtk-tune-metrics");m.appendChild(ue);let fe=h("div","dtk-tune-grid"),T=h("div","dtk-tune-controls"),O=h("div","dtk-tune-main");fe.appendChild(T),fe.appendChild(O),m.appendChild(fe);let be=h("div","dtk-tune-trim"),ie=h("div","dtk-tune-trim-head");ie.appendChild(Ye("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 Le=h("span","dtk-tune-trim-val");ie.appendChild(Le),be.appendChild(ie);let K=h("input","dtk-range");K.type="range",K.min=String(Math.min(e,200)),K.max=String(e),K.step=String(Math.max(1,Math.round(e/200))),K.value=String(e),be.appendChild(K),O.appendChild(be);let Xe=h("div","dtk-tune-chart"),Ze=h("canvas");Xe.appendChild(Ze);let X=h("div","dtk-tune-spin");X.appendChild(h("span","dtk-spin-ring")),X.appendChild(h("span","dtk-spin-txt","computing\u2026")),Xe.appendChild(X),O.appendChild(Xe);let Se=h("div","dtk-tune-legend"),se=(r,l,f)=>{let P=h("span","dtk-leg-item");P.title=f,P.appendChild(h("span",`dtk-leg-sw ${r}`)),P.appendChild(h("span","dtk-leg-txt",l)),Se.appendChild(P)};se("line","metric","The metric value over time."),se("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),se("center","band center","The expected value at the middle of the band."),se("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),se("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),O.appendChild(Se);let Je=h("div","dtk-tune-readout");O.appendChild(Je);let ge=h("div","dtk-tune-warn");ge.style.display="none",O.appendChild(ge);let Re=r=>{let l=r.seasonalityComponents,f=dt(l),P=r.minSamplesPerGroup*f;l&&f>0&&r.windowSize<P?(ge.textContent=`\u26A0 Seasonality inactive at this window: ${r.windowSize} < ${P} (min_samples_per_group ${r.minSamplesPerGroup} \xD7 ${f} key${f===1?"":"s"}). Each point keeps only ~${Math.floor(r.windowSize/f)} same-key point(s), so the band falls back to global statistics (seasonality has no effect). Raise the window to \u2265 ${P}.`,ge.style.display=""):ge.style.display="none"},Qe=h("div","dtk-tune-labhead");Qe.appendChild(Ye("Real incidents","Drag on this chart to mark each real incident span \u2014 drag its edges to adjust, drag the middle to move, click its \u2715 (or select + Delete) to remove. Pan via the strip below, scroll to zoom (both charts move together). The metrics above update as you tune.")),Qe.appendChild(h("span","dtk-tune-labhint","drag to mark \xB7 edges adjust \xB7 \u2715/Delete remove \xB7 strip pans")),O.appendChild(Qe);let ze=h("div","dtk-tune-chart dtk-tune-labchart"),He=h("canvas");ze.appendChild(He),O.appendChild(ze);let xt=(r,l)=>{let f=t.interval_seconds*1e3/2,P=(ye,Ae)=>ye>=Ae.start-f&&ye<=Ae.end+f,_=0;for(let ye of r)l.some(Ae=>P(ye,Ae))&&_++;let V=0;for(let ye of l)r.some(Ae=>P(Ae,ye))&&V++;let me=r.length;return{realIncidents:l.length,caught:V,recall:l.length?V/l.length:NaN,totalAlerts:me,correctAlerts:_,falseAlerts:me-_,fdr:me?(me-_)/me:NaN}},et=r=>Number.isFinite(r)?`${Math.round(r*100)}%`:"\u2014",Be=(r,l,f,P)=>`<span class="dtk-m-chip"><span class="dtk-m-dot" style="background:${P}"></span><span class="dtk-m-v">${r}</span><span class="dtk-m-l">${l}</span>`+(f?`<span class="dtk-m-sub">${f}</span>`:"")+"</span>";function Oe(){let r=xt(y,n),l=r.realIncidents>0,f;l?r.totalAlerts===0?f="no alerts":r.falseAlerts===0?f=`${et(r.fdr)} \xB7 all correct`:f=`${et(r.fdr)} \xB7 \u22481 in ${Math.max(1,Math.round(r.totalAlerts/r.falseAlerts))} false`:f="mark incidents to measure",ue.innerHTML=Be(String(r.realIncidents),"real incidents","","var(--c)")+Be(l?`${r.caught}/${r.realIncidents}`:"\u2014","caught",l?`recall ${et(r.recall)}`:"mark incidents","var(--green)")+Be(String(r.totalAlerts),"alerts","","var(--c)")+Be(l?String(r.falseAlerts):"\u2014","false alerts",f,"var(--anom)")}let he,ve=At(Ze,{navigable:!0,showNavigator:!1,yFit:"data",onViewChange:(r,l)=>he.setViewWindow(r,l),onHover:r=>{if(!r||!r.point||!r.point.scored){Je.textContent="";return}let l=r.point;Je.textContent=`t=${Dt(l.timestamp)} value=${lt(l.value)} band=[${lt(l.lower)}, ${lt(l.upper)}]`+(l.isAnomaly?` \u26A0 ${l.direction} (sev ${l.severity.toFixed(2)})`:"")}}),yt={type:"mad",threshold:3,windowSize:100,minSamples:30,inputType:"values",smoothing:"none",smoothingAlpha:.3,smoothingWindow:10,windowWeights:"none",halfLife:null,detrend:"none",seasonalityComponents:null,minSamplesPerGroup:10,consecutiveAnomalies:1};he=At(He,{navigable:!0,labeling:!0,yFit:"data",onViewChange:(r,l)=>ve.setViewWindow(r,l),onIncidentsChange:()=>{J(),Oe(),ve.setIncidents(n)}}),he.setIncidents(n);let tt=()=>{he.render({series:A,scored:[],params:yt,alerts:[]})},Me=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 j(e,n,t){let o=un(e),i=o.map(b=>e[b]),r=o.map(b=>n[b]),s=C(r),c=new Array(r.length),u=0;for(let b=0;b<r.length;b++)u+=r[b],c[b]=(u-.5*r[b])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return j(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(a=>a>o),r=i.map(a=>!a),s=0,c=0;for(let a=0;a<n.length;a++)i[a]?s++:c++;if(s<2||c<2)return 0;let u=(a,M)=>M.filter((x,w)=>a[w]),d=u(r,e),b=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),m=u(i,t),A=P(d,h),p=P(b,m),S=P(N,h),g=P(y,m);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=j(n,t,25),i=j(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 F(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,b,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(b=n.upperBound)!=null?b:null,u=[];for(let y=0;y<i;y++){let h=o[y],m=r[y],A=t[y];if(Number.isNaN(m)){u.push(F(y,A,h,m,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&m<s&&(p=!0,S="below",g=s-m),c!==null&&m>c&&(p=!0,S="above",g=m-c);let a=0;if(p)if(s!==null&&c!==null){let v=c-s;a=v>0?g/v:1/0}else a=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:m,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:a,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),b=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 m=0;m<c;m++){let A=s[m],p=d[m],S=r[m];if(Number.isNaN(p)){h.push(F(m,S,A,p,"missing_data"));continue}let g=Math.max(0,m-n.windowSize),a=m-g,M=d.slice(g,m),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(F(m,S,A,p,"insufficient_data"));continue}let v=new Array(a);for(let l=0;l<a;l++)v[l]=a-l;let L=[];for(let l=0;l<a;l++)x[l]&&L.push(v[l]);let B=Q(L,y),W=0;n.detrend==="linear"&&(W=Sn(w,L,B));let nn=W!==0?w.map((l,R)=>l+W*L[R]):w,G=U(t,nn,B),_={...G};if(b&&N){let l=N[m];for(let R of n.seasonalityComponents){let K=new Array(a).fill(!0);for(let f=0;f<a;f++){let q=N[g+f],T=!0;for(let V of R)if(!q||q[V]!==(l==null?void 0:l[V])){T=!1;break}K[f]=T}let z=[],E=[];for(let f=0;f<a;f++)x[f]&&K[f]&&(z.push(M[f]),E.push(v[f]));if(z.length<n.minSamplesPerGroup)continue;let tn=Q(E,y),on=W!==0?z.map((f,q)=>f+W*E[q]):z,rn=U(t,on,tn);for(let[f,q]of i){let T=G[f],sn=(q==="spread"?T>0:T!==0)?rn[f]/T:1;_[f]*=sn}}}let[D,k]=Nn(t,_,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,O=null,J=0;if(H){let l;p<D?(O="below",l=D-p):(O="above",l=p-k),J=An(t,_,l)}let en=Mn(t,_);h.push({index:m,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:O,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 I=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 vn(e,n,t){let o=[],i=0;for(let r=0;r<e.length;r++){if(!(e[r].scored&&e[r].isAnomaly)){i=0;continue}let c=r>0&&e[r-1].scored&&e[r-1].isAnomaly,u=r>0&&e[r].timestamp-e[r-1].timestamp===n;i=c&&u?i+1:1,i===t&&o.push(r)}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){I=n.series;return}if(n.type==="run"&&I){let t=n.params,o=Z(I,t);xn(o,t.direction);let i=I.intervalSeconds*1e3,r=vn(o,i,t.consecutiveAnomalies),s=$(I,t),c=0;for(let u of o)u.scored&&u.isAnomaly&&c++;self.postMessage({type:"result",id:n.id,scored:o,fires:r,eff:s,flagged:c})}};})();
2
+ `],{type:"text/javascript"})));Me.postMessage({type:"series",series:A});let Fe=0,qe=null;Me.onmessage=r=>{let l=r.data;if(l.type!=="result"||l.id!==Fe||!qe)return;X.classList.remove("on");let f=qe,P=l.fires.map(V=>A.timestamps[V]),_=P.map(V=>({t:V,kind:"anomaly"}));ve.render({series:A,scored:l.scored,params:f,alerts:_,incidents:n}),y=P,Oe(),w.textContent=`${l.flagged} flagged \xB7 ${l.fires.length} alert${l.fires.length===1?"":"s"} \xB7 warm-up ${l.eff} pts`,Re(f),oe.textContent=en(f,E)},Me.onerror=()=>{X.classList.remove("on"),w.textContent="recompute failed \u2014 see the browser console"};let ee=()=>{qe=De(),Fe+=1,X.classList.add("on"),Me.postMessage({type:"run",id:Fe,params:qe})},Pe=r=>r>1?vt(v.timestamps[e-1]-v.timestamps[e-r]):"\u2014",ae=r=>{Le.textContent=r>=e?`${e} pts \xB7 full (${Pe(e)})`:`${r} pts \xB7 ${Pe(r)}`};ae(e);function te(r){A=I(r),Me.postMessage({type:"series",series:A}),tt(),ne()}let Ce=0;K.oninput=()=>{let r=Number(K.value);ae(r),Ce&&window.clearTimeout(Ce),Ce=window.setTimeout(()=>te(r),200)};let nt=0,ne=()=>{nt&&window.clearTimeout(nt),nt=window.setTimeout(ee,130)},Ne=at("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],p.type,r=>{var l;ut((l=Zt[r])!=null?l:3),S(),ne()},"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).");T.appendChild(Ne.row);let je=Ge("Lower bound",{min:Y,max:L,step:C,value:j,fmt:r=>lt(r),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)."},ne);T.appendChild(je.row);let Ve=Ge("Upper bound",{min:Y,max:L,step:C,value:Q,fmt:r=>lt(r),hint:"Manual bounds: values above this read as anomalous."},ne);T.appendChild(Ve.row);let Te=Ge("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:p.threshold,fmt:r=>r.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},ne);T.appendChild(Te.row);let ot=Te.row.querySelector("input"),rt=Te.row.querySelector(".dtk-ctl-val"),ut=r=>{ot&&(ot.value=String(r)),rt&&(rt.textContent=r.toFixed(1))},mt=Math.max(50,Math.min(2e3,Math.floor(e/2))),pt=Math.max(mt,p.windowSize),it=Ge("Window size (points)",{min:Math.max(1,Math.min(10,p.windowSize)),max:pt,step:1,value:p.windowSize,fmt:r=>`${r} \xB7 ${vt(r*t.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."},ne);T.appendChild(it.row);let Ee=at("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],p.windowWeights,()=>{S(),ne()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");T.appendChild(Ee.row);let ft=Ge("Half-life (points)",{min:1,max:pt,step:1,value:(Nt=p.halfLife)!=null?Nt:Math.max(5,Math.round(p.windowSize/20)),fmt:r=>`${r} \xB7 ${vt(r*t.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."},ne),st=ft.row;st.style.display=p.windowWeights==="exponential"?"":"none",T.appendChild(st);let o=at("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],p.detrend,ne,"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.");T.appendChild(o.row);let s=at("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],p.smoothing,ne,"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.");T.appendChild(s.row);let u=null;if(t.seasonality_columns.length){let r=t.seasonality_columns,l=h("div","dtk-ctl");u=l,l.appendChild(Ye("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(r.length,6),P=[{label:"\u2014",value:"0"}];for(let _=1;_<=f;_++)P.push({label:`G${_}`,value:String(_)});r.forEach(_=>{let V=h("div","dtk-season-row");V.appendChild(h("span","dtk-season-col",_));let me=h("div","dtk-seg dtk-season-seg"),ye=[],Ae=()=>{var pe;return(pe=U.get(_))!=null?pe:0},_t=()=>ye.forEach(pe=>pe.classList.toggle("on",Number(pe.dataset.v)===Ae()));P.forEach(pe=>{let Ue=h("button","dtk-seg-btn",pe.label);Ue.type="button",Ue.dataset.v=pe.value,Ue.title=pe.value==="0"?`ignore ${_}`:`put ${_} in group ${pe.value}`,Ue.onclick=()=>{U.set(_,Number(pe.value)),_t(),ne()},ye.push(Ue),me.appendChild(Ue)}),_t(),V.appendChild(me),l.appendChild(V)}),T.appendChild(l)}let c=at("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(Et=t.direction)!=null?Et:"any",ne,"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.");T.appendChild(c.row);let a=Ge("Alert: consecutive anomalies",{min:1,max:10,step:1,value:E,fmt:r=>String(r),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},r=>{E=r,ne()});T.appendChild(a.row);let d=h("div","dtk-ctl"),b=h("label","dtk-check"),k=h("input");k.type="checkbox",k.onchange=()=>{ve.setZeroLine(k.checked),he.setZeroLine(k.checked)},b.title="Draw a horizontal line at y = 0 and include zero in the scale \u2014 for real-valued metrics best read relative to zero.",b.appendChild(k),b.appendChild(document.createTextNode(" Show y = 0 line")),d.appendChild(b),T.appendChild(d);let z=h("div","dtk-ctl dtk-incidents");z.appendChild(Ye("Marked incidents","The real incidents marked on the lower chart. Edit a label, focus to zoom both charts to it, or remove it. Save the set below to incidents/<metric>/ \u2014 the same store dtk autotune reads."));let H=h("div","dtk-inc-list");z.appendChild(H),T.appendChild(z);function B(r){let l=Math.max((r.end-r.start)*.5,t.interval_seconds*1e3*5);ve.setViewWindow(r.start-l,r.end+l),he.setViewWindow(r.start-l,r.end+l)}function Z(r){let l=n.indexOf(r);l>=0&&n.splice(l,1),he.setIncidents(n),ve.setIncidents(n),J(),Oe()}function J(){H.innerHTML="";let r=[...n].sort((l,f)=>l.start-f.start);if(!r.length){H.appendChild(h("div","dtk-inc-empty","None yet \u2014 drag across the lower chart to mark one."));return}for(let l of r){let f=h("div","dtk-inc-row");f.appendChild(h("span","dtk-inc-span",`${Dt(l.start)} \u2192 ${Dt(l.end)}`)),f.appendChild(h("span","dtk-inc-dur",vt(Math.max(0,l.end-l.start))));let P=h("input","dtk-inc-label");P.type="text",P.value=l.label||"",P.placeholder="label (optional)",P.oninput=()=>{l.label=P.value},f.appendChild(P);let _=h("button","dtk-inc-btn","focus");_.type="button",_.onclick=()=>B(l),f.appendChild(_);let V=h("button","dtk-inc-btn dtk-inc-del","\u2715");V.type="button",V.title="remove this incident",V.onclick=()=>Z(l),f.appendChild(V),H.appendChild(f)}}let x=[Te.row,it.row,Ee.row,o.row,s.row];u&&x.push(u);function S(){let r=Ne.get()==="manual_bounds";for(let l of x)l.style.display=r?"none":"";je.row.style.display=r?"":"none",Ve.row.style.display=r?"":"none",st.style.display=!r&&Ee.get()==="exponential"?"":"none"}S();let w=h("div","dtk-tune-stat");O.appendChild(w);let $=h("div","dtk-tune-cfg");$.appendChild(h("span","dtk-tune-cfg-k","// effective config"));let oe=h("code","dtk-tune-cfg-v");if($.appendChild(oe),O.appendChild($),t.save_url){let r=h("div","dtk-tune-apply"),l=h("button","dtk-apply-btn","Apply to metric");l.type="button",l.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=h("span","dtk-apply-msg");l.onclick=()=>{let P=De();l.disabled=!0,f.className="dtk-apply-msg info",f.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:P.type,params:qt(P)},consecutive_anomalies:E})}).then(_=>_.ok?_.json():_.text().then(V=>{throw new Error(V||`HTTP ${_.status}`)})).then(_=>{var V;f.className="dtk-apply-msg ok",f.textContent=`Applied \u2192 ${(V=_.saved)!=null?V:"metric"} (previous archived). You can close this tab.`}).catch(_=>{l.disabled=!1,f.className="dtk-apply-msg err",f.textContent=`Apply failed: ${_.message}`})},r.appendChild(l),r.appendChild(f),O.appendChild(r)}else O.appendChild(h("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 _e=r=>'"'+r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',Lt=r=>new Date(r).toISOString().slice(0,19).replace("T"," "),jt=()=>{let r=[`metric: ${t.metric}`,"timezone: UTC"],l=[...n].sort((f,P)=>f.start-P.start);if(!l.length)return r.push("incidents: []"),r.join(`
3
+ `)+`
4
+ `;r.push("incidents:");for(let f of l){let P=f.label?`, label: ${_e(f.label)}`:"";r.push(` - {start: "${Lt(f.start)}", end: "${Lt(f.end)}"${P}}`)}return r.join(`
5
+ `)+`
6
+ `},ht=h("div","dtk-tune-apply"),bt=h("input","dtk-setname");bt.type="text",bt.placeholder="name this set (optional)";let gt=h("button","dtk-apply-btn dtk-labels-btn",t.labels_save_url?"Save incidents":"Download incidents");gt.type="button",gt.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 xe=h("span","dtk-apply-msg");gt.onclick=()=>{let r=jt();if(!t.labels_save_url){let l=new Blob([r],{type:"text/yaml"}),f=h("a");f.href=URL.createObjectURL(l),f.download=`${t.metric}.yml`,f.click(),URL.revokeObjectURL(f.href),xe.className="dtk-apply-msg ok",xe.textContent=`Downloaded \u2014 drop it into incidents/${t.metric}/`;return}xe.className="dtk-apply-msg info",xe.textContent="Saving\u2026",fetch(t.labels_save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:bt.value,yaml:r})}).then(l=>l.ok?l.json():l.text().then(f=>{throw new Error(f||`HTTP ${l.status}`)})).then(l=>{var f;xe.className="dtk-apply-msg ok",xe.textContent=`Saved \u2192 ${(f=l.saved)!=null?f:"incidents"} (keep tuning, or Apply the detector)`}).catch(l=>{xe.className="dtk-apply-msg err",xe.textContent=`Save failed: ${l.message}`})},ht.appendChild(bt),ht.appendChild(gt),ht.appendChild(xe),O.appendChild(ht),tt(),J(),ee(),Oe();let wt=0;window.addEventListener("resize",()=>{wt&&cancelAnimationFrame(wt),wt=requestAnimationFrame(()=>{ve.resize(),he.resize()})})}function lt(t){if(!Number.isFinite(t))return"\u2014";let i=Math.abs(t);return i!==0&&(i<.01||i>=1e6)?t.toExponential(2):Number(t.toFixed(i<1?4:2)).toString()}function Dt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function vt(t){let i=Math.round(t/6e4);if(i<60)return`${i}m`;let m=Math.floor(i/60);if(m<24)return`${m}h`;let e=Math.floor(m/24),v=m%24;return v?`${e}d ${v}h`:`${e}d`}function nn(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var Ft=!1;function on(){if(Ft)return;Ft=!0;let t=`
7
+ .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
8
+ --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
9
+ --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
10
+ --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
11
+ .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
12
+ .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
13
+ .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
14
+ .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
15
+ color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
16
+ .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
17
+ .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
18
+ .dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
19
+ @media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
20
+ .dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
21
+ border:1px solid var(--border);border-radius:12px;padding:16px;}
22
+ .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
23
+ .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
24
+ .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
25
+ .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
26
+ .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
27
+ .dtk-seg.dtk-wrap{flex-wrap:wrap;}
28
+ .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
29
+ font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
30
+ .dtk-seg-btn:hover{color:var(--ink);}
31
+ .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
32
+ .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
33
+ .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
34
+ .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
35
+ .dtk-tune-chart{position:relative;width:100%;height:470px;background:var(--surface);
36
+ border:1px solid var(--border);border-radius:12px;overflow:hidden;}
37
+ .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
38
+ .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
39
+ .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
40
+ .dtk-tune-warn{font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--c7);
41
+ background:rgba(240,173,78,0.13);border:1px solid rgba(240,173,78,0.5);border-radius:8px;padding:8px 11px;}
42
+ .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
43
+ font-size:12px;overflow-x:auto;}
44
+ .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
45
+ .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
46
+ .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
47
+ .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
48
+ font-size:14px;font-weight:600;cursor:pointer;}
49
+ .dtk-apply-btn:hover{background:var(--c7);}
50
+ .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
51
+ .dtk-apply-msg{font-size:13px;}
52
+ .dtk-apply-msg.ok{color:var(--green);}
53
+ .dtk-apply-msg.err{color:var(--anom);}
54
+ .dtk-apply-msg.info{color:var(--muted);}
55
+ .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
56
+ border-radius:8px;padding:10px 12px;}
57
+ .dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
58
+ .dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
59
+ border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
60
+ .dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
61
+ .dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
62
+ .dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
63
+ background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
64
+ padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
65
+ .dtk-tune-spin.on{display:inline-flex;}
66
+ .dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
67
+ border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
68
+ @keyframes dtk-spin{to{transform:rotate(360deg);}}
69
+ .dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
70
+ .dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
71
+ .dtk-leg-sw{display:inline-block;flex:0 0 auto;}
72
+ .dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
73
+ .dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
74
+ border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
75
+ .dtk-leg-sw.center{width:16px;height:2px;
76
+ background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
77
+ .dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
78
+ .dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
79
+ border-top:7px solid var(--anom);}
80
+ .dtk-leg-txt{white-space:nowrap;}
81
+ .dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
82
+ .dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
83
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
84
+ .dtk-season-seg{flex:0 0 auto;padding:2px;}
85
+ .dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
86
+ .dtk-tune-metrics{display:flex;flex-wrap:wrap;gap:10px;margin:16px 0 2px;}
87
+ .dtk-m-chip{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;background:var(--surface);
88
+ border:1px solid var(--border);border-radius:10px;font-size:13px;}
89
+ .dtk-m-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;}
90
+ .dtk-m-v{font-family:var(--mono);font-weight:700;font-size:15px;color:var(--ink);}
91
+ .dtk-m-l{color:var(--faint);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
92
+ .dtk-m-sub{color:var(--muted);font-family:var(--mono);font-size:11.5px;}
93
+ .dtk-tune-labhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
94
+ margin-top:4px;flex-wrap:wrap;}
95
+ .dtk-tune-labhint{font-family:var(--mono);font-size:11px;color:var(--faint);}
96
+ .dtk-tune-labchart{height:240px;}
97
+ .dtk-incidents{gap:8px;}
98
+ .dtk-inc-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow:auto;}
99
+ .dtk-inc-empty{font-size:12px;color:var(--faint);font-style:italic;}
100
+ .dtk-inc-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:var(--paper);
101
+ border:1px solid var(--border);border-radius:7px;padding:6px 8px;}
102
+ .dtk-inc-span{font-family:var(--mono);font-size:10.5px;color:var(--ink);}
103
+ .dtk-inc-dur{font-family:var(--mono);font-size:10.5px;color:var(--muted);}
104
+ .dtk-inc-label{flex:1 1 90px;min-width:70px;background:var(--surface);color:var(--ink);
105
+ border:1px solid var(--border);border-radius:5px;padding:4px 7px;font-family:var(--sans);font-size:11.5px;}
106
+ .dtk-inc-label:focus{outline:none;border-color:var(--c);}
107
+ .dtk-inc-btn{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;
108
+ padding:3px 8px;font-size:11px;cursor:pointer;font-family:var(--sans);}
109
+ .dtk-inc-btn:hover{border-color:var(--c);color:var(--c7);}
110
+ .dtk-inc-del{color:var(--anom);}
111
+ .dtk-setname{background:var(--surface);color:var(--ink);border:1px solid var(--border);border-radius:8px;
112
+ padding:9px 11px;font-family:var(--sans);font-size:13px;min-width:180px;}
113
+ .dtk-setname::placeholder{color:var(--faint);}
114
+ .dtk-setname:focus{outline:none;border-color:var(--c);}
115
+ .dtk-labels-btn{background:var(--surface);color:var(--ink);border:1px solid var(--border);}
116
+ .dtk-labels-btn:hover{background:var(--paper);border-color:var(--c);color:var(--c7);}
117
+ `,i=document.createElement("style");i.textContent=t,document.head.appendChild(i)}window.__DTK_TUNE__={render:tn};})();
@@ -145,6 +145,7 @@ def build_tune_payload(
145
145
  end: datetime | None = None,
146
146
  project_name: str | None = None,
147
147
  save_url: str | None = None,
148
+ incidents: list[dict[str, str]] | None = None,
148
149
  ) -> dict[str, Any]:
149
150
  """Assemble the interactive tuning payload from the persisted ``_dtk_datapoints``.
150
151
 
@@ -152,7 +153,13 @@ def build_tune_payload(
152
153
  is ``None`` for a static (read-only, no write-back) preview. With no explicit
153
154
  ``start``/``end`` the window defaults to a budget-sized recent slice
154
155
  (``default_window_points``), not the whole history.
156
+
157
+ ``incidents`` seeds the synced labeler with already-marked spans (display dicts
158
+ ``{start, end, label}`` from ``incidents_to_display``). ``labels_save_url`` (the
159
+ POST endpoint for **Save incidents**) is injected by the server, like
160
+ ``save_url`` — it is ``None`` here.
155
161
  """
162
+ seed_incidents = incidents or []
156
163
  name = metric_config.name
157
164
  interval = metric_config.get_interval()
158
165
  interval_seconds = interval.seconds
@@ -190,6 +197,8 @@ def build_tune_payload(
190
197
  "consecutive_anomalies": consecutive,
191
198
  "direction": direction,
192
199
  "save_url": save_url,
200
+ "incidents": seed_incidents,
201
+ "labels_save_url": None,
193
202
  }
194
203
  if start is None or end is None:
195
204
  return empty
@@ -228,4 +237,6 @@ def build_tune_payload(
228
237
  "consecutive_anomalies": consecutive,
229
238
  "direction": direction,
230
239
  "save_url": save_url,
240
+ "incidents": seed_incidents,
241
+ "labels_save_url": None,
231
242
  }