detectkit 0.30.1__tar.gz → 0.31.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {detectkit-0.30.1/detectkit.egg-info → detectkit-0.31.0}/PKG-INFO +15 -5
  2. {detectkit-0.30.1 → detectkit-0.31.0}/README.md +11 -2
  3. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/branding.py +9 -2
  5. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/html_labeler.py +69 -7
  6. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/cli.md +4 -2
  7. detectkit-0.31.0/detectkit/tuning/assets/tune.js +80 -0
  8. {detectkit-0.30.1 → detectkit-0.31.0/detectkit.egg-info}/PKG-INFO +15 -5
  9. {detectkit-0.30.1 → detectkit-0.31.0}/pyproject.toml +3 -2
  10. detectkit-0.30.1/detectkit/tuning/assets/tune.js +0 -51
  11. {detectkit-0.30.1 → detectkit-0.31.0}/LICENSE +0 -0
  12. {detectkit-0.30.1 → detectkit-0.31.0}/MANIFEST.in +0 -0
  13. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/__init__.py +0 -0
  14. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/__init__.py +0 -0
  15. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/base.py +0 -0
  16. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/email.py +0 -0
  17. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/factory.py +0 -0
  18. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/mattermost.py +0 -0
  19. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/slack.py +0 -0
  20. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/telegram.py +0 -0
  21. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/channels/webhook.py +0 -0
  22. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  23. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  24. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  25. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  26. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  27. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  28. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  29. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  30. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  31. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/__init__.py +0 -0
  32. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/_base.py +0 -0
  33. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/_types.py +0 -0
  34. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/autotuner.py +0 -0
  35. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/config_emitter.py +0 -0
  36. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/crossval.py +0 -0
  37. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/detector_select.py +0 -0
  38. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/distribution.py +0 -0
  39. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/grid_search.py +0 -0
  40. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/label_server.py +0 -0
  41. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/labels.py +0 -0
  42. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/result.py +0 -0
  43. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/scoring.py +0 -0
  44. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/seasonality_search.py +0 -0
  45. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/settings.py +0 -0
  46. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/autotune/window_select.py +0 -0
  47. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/__init__.py +0 -0
  48. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/_output.py +0 -0
  49. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  50. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  51. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  52. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  55. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  56. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  57. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  58. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  59. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  60. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/__init__.py +0 -0
  61. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/autotune.py +0 -0
  62. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/clean.py +0 -0
  63. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/init.py +0 -0
  64. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/init_claude.py +0 -0
  65. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/run.py +0 -0
  66. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/test_alert.py +0 -0
  67. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/tune.py +0 -0
  68. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/commands/unlock.py +0 -0
  69. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/cli/main.py +0 -0
  70. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/config/__init__.py +0 -0
  71. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/config/metric_config.py +0 -0
  72. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/config/profile.py +0 -0
  73. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/config/project_config.py +0 -0
  74. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/config/validator.py +0 -0
  75. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/core/__init__.py +0 -0
  76. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/core/interval.py +0 -0
  77. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/core/models.py +0 -0
  78. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/__init__.py +0 -0
  79. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/_sql_manager.py +0 -0
  80. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/clickhouse_manager.py +0 -0
  81. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/__init__.py +0 -0
  82. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  83. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  84. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_base.py +0 -0
  85. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  86. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_detections.py +0 -0
  87. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  88. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  89. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_schema.py +0 -0
  90. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  91. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/internal_tables/manager.py +0 -0
  92. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/manager.py +0 -0
  93. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/mysql_manager.py +0 -0
  94. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/postgres_manager.py +0 -0
  95. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/database/tables.py +0 -0
  96. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/__init__.py +0 -0
  97. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/base.py +0 -0
  98. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/factory.py +0 -0
  99. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/seasonality.py +0 -0
  100. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/__init__.py +0 -0
  101. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  102. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/iqr.py +0 -0
  103. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/mad.py +0 -0
  104. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  105. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/detectors/statistical/zscore.py +0 -0
  106. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/loaders/__init__.py +0 -0
  107. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/loaders/metric_loader.py +0 -0
  108. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/loaders/query_template.py +0 -0
  109. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/__init__.py +0 -0
  110. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/error_dispatch.py +0 -0
  111. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  112. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  113. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  114. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  115. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  116. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  117. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  118. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/reporting/__init__.py +0 -0
  119. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/reporting/assets/report.js +0 -0
  120. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/reporting/builder.py +0 -0
  121. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/reporting/html_report.py +0 -0
  122. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/tuning/__init__.py +0 -0
  123. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/tuning/config_writer.py +0 -0
  124. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/tuning/html.py +0 -0
  125. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/tuning/payload.py +0 -0
  126. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/tuning/server.py +0 -0
  127. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/utils/__init__.py +0 -0
  128. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/utils/datetime_utils.py +0 -0
  129. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/utils/env_interpolation.py +0 -0
  130. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/utils/json_utils.py +0 -0
  131. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit/utils/stats.py +0 -0
  132. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit.egg-info/SOURCES.txt +0 -0
  133. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit.egg-info/dependency_links.txt +0 -0
  134. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit.egg-info/entry_points.txt +0 -0
  135. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit.egg-info/requires.txt +0 -0
  136. {detectkit-0.30.1 → detectkit-0.31.0}/detectkit.egg-info/top_level.txt +0 -0
  137. {detectkit-0.30.1 → detectkit-0.31.0}/requirements.txt +0 -0
  138. {detectkit-0.30.1 → detectkit-0.31.0}/setup.cfg +0 -0
  139. {detectkit-0.30.1 → detectkit-0.31.0}/setup.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.30.1
3
+ Version: 0.31.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/alexeiveselov92/detectkit
8
- Project-URL: Documentation, https://github.com/alexeiveselov92/detectkit
7
+ Project-URL: Homepage, https://dtk.pipelab.dev
8
+ Project-URL: Documentation, https://dtk.pipelab.dev
9
9
  Project-URL: Repository, https://github.com/alexeiveselov92/detectkit
10
+ Project-URL: Changelog, https://github.com/alexeiveselov92/detectkit/blob/main/CHANGELOG.md
10
11
  Project-URL: Issues, https://github.com/alexeiveselov92/detectkit/issues
11
12
  Keywords: monitoring,anomaly-detection,metrics,timeseries,alerting
12
13
  Classifier: Development Status :: 3 - Alpha
@@ -66,14 +67,22 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
66
67
  Requires-Dist: pymysql>=1.0.0; extra == "integration"
67
68
  Dynamic: license-file
68
69
 
69
- # detectkit
70
+ <a href="https://dtk.pipelab.dev">
71
+ <picture>
72
+ <source media="(prefers-color-scheme: dark)" srcset="assets/detectkit-lockup-dark.svg">
73
+ <img src="assets/detectkit-lockup.svg" alt="detectkit" width="260">
74
+ </picture>
75
+ </a>
70
76
 
71
77
  [![PyPI version](https://img.shields.io/pypi/v/detectkit.svg)](https://pypi.org/project/detectkit/)
72
78
  [![Python](https://img.shields.io/pypi/pyversions/detectkit.svg)](https://pypi.org/project/detectkit/)
79
+ [![Docs & playground](https://img.shields.io/badge/docs-dtk.pipelab.dev-d15b36)](https://dtk.pipelab.dev)
73
80
 
74
81
  **Metric monitoring with automatic anomaly detection.**
75
82
 
76
- `detectkit` is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
83
+ [`detectkit`](https://dtk.pipelab.dev) is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
84
+
85
+ **Website, docs & live playground → [dtk.pipelab.dev](https://dtk.pipelab.dev)**
77
86
 
78
87
  ## Features
79
88
 
@@ -181,6 +190,7 @@ for r in results:
181
190
 
182
191
  ## Documentation
183
192
 
193
+ - **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
184
194
  - [Getting Started](docs/getting-started/quickstart.md) — 5-minute quickstart
185
195
  - [Configuration Guide](docs/guides/configuration.md) — all config options
186
196
  - [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
@@ -1,11 +1,19 @@
1
- # detectkit
1
+ <a href="https://dtk.pipelab.dev">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/detectkit-lockup-dark.svg">
4
+ <img src="assets/detectkit-lockup.svg" alt="detectkit" width="260">
5
+ </picture>
6
+ </a>
2
7
 
3
8
  [![PyPI version](https://img.shields.io/pypi/v/detectkit.svg)](https://pypi.org/project/detectkit/)
4
9
  [![Python](https://img.shields.io/pypi/pyversions/detectkit.svg)](https://pypi.org/project/detectkit/)
10
+ [![Docs & playground](https://img.shields.io/badge/docs-dtk.pipelab.dev-d15b36)](https://dtk.pipelab.dev)
5
11
 
6
12
  **Metric monitoring with automatic anomaly detection.**
7
13
 
8
- `detectkit` is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
14
+ [`detectkit`](https://dtk.pipelab.dev) is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
15
+
16
+ **Website, docs & live playground → [dtk.pipelab.dev](https://dtk.pipelab.dev)**
9
17
 
10
18
  ## Features
11
19
 
@@ -113,6 +121,7 @@ for r in results:
113
121
 
114
122
  ## Documentation
115
123
 
124
+ - **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
116
125
  - [Getting Started](docs/getting-started/quickstart.md) — 5-minute quickstart
117
126
  - [Configuration Guide](docs/guides/configuration.md) — all config options
118
127
  - [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
@@ -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.30.1"
7
+ __version__ = "0.31.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -12,12 +12,19 @@ the brand avatar URL and name have a single source of truth.
12
12
  # ``username`` and the email ``From`` display name).
13
13
  BRAND_USERNAME = "detectkit"
14
14
 
15
+ # Public detectkit site — landing, docs, and the served brand assets. This is the
16
+ # **single source of truth** for the project's URL inside the package: the icon
17
+ # and alert-guide links below are derived from it, so if the domain ever changes
18
+ # only this line needs updating. (Docs/website Markdown and ``pyproject``
19
+ # metadata reference the URL directly and are updated in place.)
20
+ BRAND_SITE_URL = "https://dtk.pipelab.dev"
21
+
15
22
  # Brand avatar served from the docs site. Slack/Mattermost render the webhook
16
23
  # bot icon from a **raster** URL (an SVG is not rendered as a bot avatar), so
17
24
  # this points at a PNG. Override per channel with ``icon_url`` (a custom image)
18
25
  # or opt out of the avatar entirely with ``icon_emoji``. The PNG is generated by
19
26
  # ``website/scripts/make-bot-icon.mjs`` and served from ``website/public/``.
20
- BRAND_ICON_URL = "https://dtk.pipelab.dev/bot-icon.png"
27
+ BRAND_ICON_URL = f"{BRAND_SITE_URL}/bot-icon.png"
21
28
 
22
29
  # Default "how to read this alert" link surfaced on every default-rendered alert,
23
30
  # so non-operator stakeholders (PMs, analysts, on-call) seeing a notification can
@@ -26,5 +33,5 @@ BRAND_ICON_URL = "https://dtk.pipelab.dev/bot-icon.png"
26
33
  # own runbook (or hide it) via ``ProjectConfig.alert_help_url`` — the resolved URL
27
34
  # is stamped onto ``AlertData.help_url`` by the orchestrator. ``ALERT_GUIDE_LABEL``
28
35
  # is the shared link text so every channel reads the same.
29
- BRAND_ALERT_GUIDE_URL = "https://dtk.pipelab.dev/guides/reading-alerts/"
36
+ BRAND_ALERT_GUIDE_URL = f"{BRAND_SITE_URL}/guides/reading-alerts/"
30
37
  ALERT_GUIDE_LABEL = "How to read this alert"
@@ -141,7 +141,7 @@ _TEMPLATE = """<!doctype html>
141
141
  box-shadow: var(--panel-shadow); }
142
142
  .zoombar { display:flex; align-items:center; gap:8px; margin: 12px 0 6px; }
143
143
  .rangelbl { margin-left: auto; color: var(--muted); font-size: 12px; font-family: var(--mono); }
144
- canvas#ov { width: 100%; height: 66px; display:block; touch-action: none;
144
+ canvas#ov { width: 100%; height: 78px; display:block; touch-action: none;
145
145
  background: var(--term-bg); border: 1px solid var(--term-border); border-radius: 12px; cursor: grab;
146
146
  box-shadow: var(--panel-shadow); }
147
147
  .navhint { color: var(--faint); font-size: 12px; margin: 8px 2px 0; line-height: 1.55; }
@@ -256,7 +256,7 @@ PRELOAD.forEach(p => { const a = Date.parse(String(p.start).replace(' ','T')+'Z'
256
256
  if (!isNaN(a) && !isNaN(b)) incidents.push({a: Math.min(a,b), b: Math.max(a,b), label: p.label || ''}); });
257
257
  const c = document.getElementById('c'), ov = document.getElementById('ov');
258
258
  const ctx = c.getContext('2d'), octx = ov.getContext('2d');
259
- const M = {l:56, r:16, t:14, b:30}, OM = {l:56, r:16, t:8, b:8};
259
+ const M = {l:56, r:16, t:14, b:30}, OM = {l:56, r:16, t:8, b:20};
260
260
  let dpr = 1, hover = null, dragging = null, ovAct = null;
261
261
  // selObj: the currently selected incident (object ref, survives re-sorting);
262
262
  // hoverRow/hoverDel: list-row / ✕-handle hover targets; threshold-capture state.
@@ -286,6 +286,42 @@ const fmtTs = ts => new Date(ts).toISOString().slice(0,19).replace('T',' ');
286
286
  const fmtTick = (ts, sp) => { const s = new Date(ts).toISOString();
287
287
  return sp < 2*86400000 ? s.slice(5,16).replace('T',' ') : s.slice(5,10); };
288
288
  const fmtVal = v => { const a = Math.abs(v); return a>=1000 ? v.toFixed(0) : a>=10 ? v.toFixed(1) : v.toFixed(2); };
289
+ // Adaptive, UTC-aligned "nice" time gridlines so each point's real time is
290
+ // readable at a glance — placed at round calendar boundaries (the hour, day,
291
+ // month, year) rather than arbitrary even splits. Returns {ticks, step}.
292
+ const MS_S=1000, MS_MIN=60*MS_S, MS_H=60*MS_MIN, MS_D=24*MS_H;
293
+ const TICK_STEPS=[MS_S,2*MS_S,5*MS_S,10*MS_S,15*MS_S,30*MS_S,
294
+ MS_MIN,2*MS_MIN,5*MS_MIN,10*MS_MIN,15*MS_MIN,30*MS_MIN,
295
+ MS_H,2*MS_H,3*MS_H,6*MS_H,12*MS_H, MS_D,2*MS_D,3*MS_D,5*MS_D,7*MS_D,14*MS_D];
296
+ function niceTimeTicks(lo, hi, target) {
297
+ const span=Math.max(hi-lo, 1);
298
+ if (span > target*28*MS_D) { // month/year calendar ticks for long spans
299
+ const mSteps=[1,2,3,6,12,24,36,60,120,240]; let stepM=mSteps[mSteps.length-1];
300
+ for (const m of mSteps) { if (span/(m*30.44*MS_D) <= target) { stepM=m; break; } }
301
+ const d=new Date(lo); let y=d.getUTCFullYear(), mo=d.getUTCMonth();
302
+ if (stepM>=12) { const ys=stepM/12; y=Math.floor(y/ys)*ys; mo=0; }
303
+ else { mo=Math.floor(mo/stepM)*stepM; }
304
+ const ticks=[]; let t=Date.UTC(y,mo,1);
305
+ while (t<=hi) { if (t>=lo) ticks.push(t); mo+=stepM; y+=Math.floor(mo/12); mo%=12; t=Date.UTC(y,mo,1); }
306
+ return {ticks, step: stepM*30*MS_D};
307
+ }
308
+ let step=TICK_STEPS[TICK_STEPS.length-1];
309
+ for (const s of TICK_STEPS) { if (span/s <= target) { step=s; break; } }
310
+ let first;
311
+ if (step % MS_D === 0) { const d=new Date(lo); // align day-multiples to UTC midnight
312
+ first=Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
313
+ const k=step/MS_D, dayNo=Math.round(first/MS_D); first += ((k - (dayNo % k)) % k) * MS_D;
314
+ } else { first=Math.ceil(lo/step)*step; }
315
+ const ticks=[]; for (let t=first; t<=hi; t+=step) if (t>=lo) ticks.push(t);
316
+ return {ticks, step};
317
+ }
318
+ // Step-aware label: year / month / day / minute / second precision.
319
+ const fmtAxis = (ts, step) => { const s=new Date(ts).toISOString();
320
+ if (step >= 320*MS_D) return s.slice(0,4);
321
+ if (step >= 26*MS_D) return s.slice(0,7);
322
+ if (step >= MS_D) return s.slice(5,10);
323
+ if (step >= MS_H) return s.slice(5,16).replace('T',' ');
324
+ return s.slice(5,19).replace('T',' '); };
289
325
  function fmtDur(ms) { const m = Math.round(ms/60000); if (m<60) return m+'m';
290
326
  const h = Math.floor(m/60), mm = m%60; if (h<24) return h+'h'+(mm?(' '+mm+'m'):'');
291
327
  const d = Math.floor(h/24), hh = h%24; return d+'d'+(hh?(' '+hh+'h'):''); }
@@ -399,10 +435,16 @@ function draw() {
399
435
  ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1*dpr;
400
436
  ctx.beginPath(); ctx.moveTo(M.l*dpr,yy); ctx.lineTo(c.width-M.r*dpr,yy); ctx.stroke();
401
437
  ctx.fillStyle='#6e675b'; ctx.textAlign='right'; ctx.fillText(fmtVal(v),(M.l-8)*dpr,yy); }
438
+ // Adaptive vertical time gridlines + round-boundary labels, so a point's place
439
+ // in real time reads off the grid (not only by chasing the cursor).
402
440
  ctx.textBaseline = 'top';
403
- for (let i=0;i<=5;i++) { const ts=viewMin+vspan()*i/5, xx=px(ts);
404
- ctx.fillStyle='#6e675b'; ctx.textAlign=i===0?'left':i===5?'right':'center';
405
- ctx.fillText(fmtTick(ts,vspan()), xx, (c.height-M.b+8)*dpr); }
441
+ const xtk=niceTimeTicks(viewMin, viewMax, 7);
442
+ xtk.ticks.forEach(ts => { const xx=px(ts);
443
+ ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1*dpr;
444
+ ctx.beginPath(); ctx.moveTo(xx, M.t*dpr); ctx.lineTo(xx, c.height-M.b*dpr); ctx.stroke();
445
+ ctx.fillStyle='#6e675b';
446
+ ctx.textAlign = xx < (M.l+24)*dpr ? 'left' : xx > c.width-(M.r+24)*dpr ? 'right' : 'center';
447
+ ctx.fillText(fmtAxis(ts, xtk.step), xx, c.height-(M.b-8)*dpr); });
406
448
  ctx.save(); ctx.beginPath(); ctx.rect(M.l*dpr, M.t*dpr, plotW(), plotH()); ctx.clip();
407
449
  // Threshold-capture preview bands (amber), under the committed incident bands.
408
450
  if (thMode) { const val=thEff(); if (val!==null) thRuns().forEach(r => {
@@ -508,20 +550,40 @@ function drawHover() {
508
550
 
509
551
  function drawOverview() {
510
552
  octx.clearRect(0,0,ov.width,ov.height);
553
+ const otk=niceTimeTicks(tmin, tmax, 5), plotBot=ov.height-OM.b*dpr;
511
554
  octx.save(); octx.beginPath(); octx.rect(OM.l*dpr, OM.t*dpr, ovWd(), ovHt()); octx.clip();
512
- incidents.forEach(iv => { const x0=ovpx(iv.a), x1=ovpx(iv.b);
513
- octx.fillStyle='rgba(214,50,50,0.30)'; octx.fillRect(x0, OM.t*dpr, x1-x0, ovHt()); });
555
+ // faint vertical time gridlines behind the series
556
+ octx.strokeStyle='rgba(201,194,180,0.10)'; octx.lineWidth=1*dpr;
557
+ otk.ticks.forEach(ts => { const xx=ovpx(ts);
558
+ octx.beginPath(); octx.moveTo(xx, OM.t*dpr); octx.lineTo(xx, plotBot); octx.stroke(); });
514
559
  drawSeries(octx, ovpx, ovpy, tmin, tmax, OM.l*dpr, ovWd(), 'rgba(209,91,54,0.7)', 1.1);
515
560
  octx.restore();
516
561
  const vx0=ovpx(viewMin), vx1=ovpx(viewMax);
562
+ // dim the out-of-view region (plot area only — leaves the label band clean)
517
563
  octx.fillStyle='rgba(27,25,22,0.55)';
518
564
  octx.fillRect(OM.l*dpr, OM.t*dpr, vx0-OM.l*dpr, ovHt());
519
565
  octx.fillRect(vx1, OM.t*dpr, (ov.width-OM.r*dpr)-vx1, ovHt());
566
+ // incident markers, full-strength on top of the dim so every incident stays
567
+ // visible to navigate to — min width so a single-point/short span isn't
568
+ // sub-pixel on the full-span strip; the selected one is highlighted.
569
+ incidents.forEach(iv => { const x0=ovpx(iv.a), w=Math.max(ovpx(iv.b)-x0, 3*dpr);
570
+ octx.fillStyle = (iv===selObj) ? 'rgba(214,50,50,0.97)' : 'rgba(214,50,50,0.72)';
571
+ octx.fillRect(x0, OM.t*dpr, w, ovHt()); });
572
+ // current-view window frame + grab handles
520
573
  octx.fillStyle='rgba(245,241,232,0.06)'; octx.fillRect(vx0, OM.t*dpr, vx1-vx0, ovHt());
521
574
  octx.strokeStyle='#d15b36'; octx.lineWidth=1.5*dpr;
522
575
  octx.strokeRect(vx0, OM.t*dpr+1, vx1-vx0, ovHt()-2);
523
576
  octx.fillStyle='#d15b36'; const hy=OM.t*dpr+ovHt()/2-9*dpr;
524
577
  octx.fillRect(vx0-2*dpr, hy, 4*dpr, 18*dpr); octx.fillRect(vx1-2*dpr, hy, 4*dpr, 18*dpr);
578
+ // absolute-time anchors under the strip, so a point's real time is readable
579
+ // across the whole series without hovering.
580
+ octx.font=(10*dpr)+'px ui-monospace, monospace'; octx.textBaseline='top';
581
+ otk.ticks.forEach(ts => { const xx=ovpx(ts);
582
+ octx.strokeStyle='rgba(201,194,180,0.25)'; octx.lineWidth=1*dpr;
583
+ octx.beginPath(); octx.moveTo(xx, plotBot); octx.lineTo(xx, plotBot+3*dpr); octx.stroke();
584
+ octx.fillStyle='#6e675b';
585
+ octx.textAlign = xx < (OM.l+26)*dpr ? 'left' : xx > ov.width-(OM.r+26)*dpr ? 'right' : 'center';
586
+ octx.fillText(fmtAxis(ts, otk.step), xx, plotBot+5*dpr); });
525
587
  }
526
588
 
527
589
  const tsAt = clientX => { const r=c.getBoundingClientRect();
@@ -78,8 +78,10 @@ Full reference: `autotune.md`.
78
78
  The **manual, interactive** sibling of `dtk autotune`. Opens a localhost browser
79
79
  view of the metric's **real** persisted series and lets you turn the detector's
80
80
  knobs (type, threshold, window, recency weighting + half-life, detrend, smoothing,
81
- seasonality conditioning, alert `consecutive_anomalies`) while the confidence band
82
- and flagged anomalies **recompute live**. Clicking **Apply** writes the chosen
81
+ **seasonality groups**, alert `consecutive_anomalies`) while the confidence band
82
+ and flagged anomalies **recompute live**. The chart is **zoomable** (scroll/drag +
83
+ a navigator strip) and a **"Points shown"** slider trims the active sample to speed
84
+ up recompute on a long metric. Clicking **Apply** writes the chosen
83
85
  config back into the metric YAML **in place** (autotune, by contrast, writes a new
84
86
  `__tuned_<id>.yml` and never edits the original). Reads the metric's loaded
85
87
  datapoints (run `dtk run --steps load` first if empty); the selector must resolve
@@ -0,0 +1,80 @@
1
+ "use strict";(()=>{var tt={"--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 O(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||tt[t]||"#888"}function nt(t){let r=t.replace("#","").trim();r.length===3&&(r=r[0]+r[0]+r[1]+r[1]+r[2]+r[2]);let u=parseInt(r,16);return r.length!==6||Number.isNaN(u)?[209,91,54]:[u>>16&255,u>>8&255,u&255]}function R(t,r){let[u,e,d]=nt(t);return`rgba(${u},${e},${d},${r})`}function Fe(t){let r=Math.max(1,window.devicePixelRatio||1),u=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(u*r),t.height=Math.round(e*r),r}var $e=Number.isFinite;function ze(t,r,u,e,d,i,p,s,m,x,f,k){let z=r.length,S=Math.max(1,Math.round(p)),A=d-e||1,ie=0;for(let W=0;W<z;W++){let N=u[W];!$e(N)||r[W]<e||r[W]>d||ie++}if(t.strokeStyle=x,t.lineWidth=f*k,t.lineJoin="round",t.beginPath(),ie<=S){let W=!1;for(let N=0;N<z;N++){let D=u[N],T=r[N];if(!$e(D)||T<e||T>d){W=!1;continue}let E=s(T),I=m(D);W?t.lineTo(E,I):(t.moveTo(E,I),W=!0)}}else{let W=new Array(S).fill(null),N=new Array(S).fill(null);for(let T=0;T<z;T++){let E=u[T],I=r[T];if(!$e(E)||I<e||I>d)continue;let P=Math.floor((I-e)/A*(S-1));P=P<0?0:P>S-1?S-1:P,(W[P]===null||E<W[P])&&(W[P]=E),(N[P]===null||E>N[P])&&(N[P]=E)}let D=!1;for(let T=0;T<S;T++){if(N[T]===null){D=!1;continue}let E=i+T,I=m(N[T]),P=m(W[T]);D?t.lineTo(E,I):(t.moveTo(E,I),D=!0),t.lineTo(E,P)}}t.stroke()}function Be(t){let r=Math.abs(t);return r>=1e3?t.toFixed(0):r>=10?t.toFixed(1):r>=1?t.toFixed(2):t.toFixed(3)}function je(t,r){let u=new Date(t).toISOString();return r<2*864e5?u.slice(5,16).replace("T"," "):u.slice(5,10)}function Ye(t,r,u){return{left:r.l*u,top:r.t*u,right:t.width-r.r*u,bottom:t.height-r.b*u}}function Ue(t,r,u,e,d,i,p){let s=Ye(r,u,e),m=Math.max(s.left,Math.min(d(i),s.right));m<=s.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(s.left,s.top,m-s.left,s.bottom-s.top),t.strokeStyle=R(O("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(m,s.top),t.lineTo(m,s.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=R(O("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(p,m+6*e,s.top+5*e),t.restore())}function Ge(t,r,u,e,d,i,p){let s=Ye(r,u,e),m=5*e;t.save();for(let x=0;x<i.length;x++){let f=i[x],k=d(f.t);if(k<s.left-1||k>s.right+1)continue;let z=p(f.kind);t.strokeStyle=R(z,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(k,s.top),t.lineTo(k,s.bottom),t.stroke(),t.fillStyle=z,t.beginPath(),t.moveTo(k-m,s.top),t.lineTo(k+m,s.top),t.lineTo(k,s.top+m*1.4),t.closePath(),t.fill()}t.restore()}var ot={mad:1,zscore:2,iqr:4};function rt(t,r){let u=t.seasonalityData;if(!u||u.length===0)return 0;let e=0;for(let d of r){let i=new Set;for(let p of u)i.add(d.map(s=>{var m;return String((m=p==null?void 0:p[s])!=null?m:"")}).join("|"));e=Math.max(e,i.size)}return e}function Ve(t,r){let u=t.timestamps.length,e=Math.max(r.minSamples,ot[r.type]);if(r.smoothing==="sma"&&(e=Math.max(e,r.smoothingWindow-1)),r.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/r.smoothingAlpha))),r.inputType!=="values"&&(e=Math.max(e,1)),r.seasonalityComponents!==null&&r.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let i=rt(t,r.seasonalityComponents);if(i>0){let p=r.minSamplesPerGroup*i;r.windowSize>=p&&(e=Math.max(e,p))}}return Math.min(e,u)}var C={l:52,r:14,t:14,b:26},Ie=46,Ke=10,it=13,te=Number.isFinite,ce=1e3,ue=60*ce,de=60*ue,q=24*de,Oe=[ce,2*ce,5*ce,10*ce,15*ce,30*ce,ue,2*ue,5*ue,10*ue,15*ue,30*ue,de,2*de,3*de,6*de,12*de,q,2*q,3*q,5*q,7*q,14*q];function Xe(t,r,u){let e=Math.max(r-t,1);if(e>u*28*q){let s=[1,2,3,6,12,24,36,60,120,240],m=s[s.length-1];for(let A of s)if(e/(A*30.44*q)<=u){m=A;break}let x=new Date(t),f=x.getUTCFullYear(),k=x.getUTCMonth();if(m>=12){let A=m/12;f=Math.floor(f/A)*A,k=0}else k=Math.floor(k/m)*m;let z=[],S=Date.UTC(f,k,1);for(;S<=r;)S>=t&&z.push(S),k+=m,f+=Math.floor(k/12),k%=12,S=Date.UTC(f,k,1);return{ticks:z,step:m*30*q}}let d=Oe[Oe.length-1];for(let s of Oe)if(e/s<=u){d=s;break}let i;if(d%q===0){let s=new Date(t);i=Date.UTC(s.getUTCFullYear(),s.getUTCMonth(),s.getUTCDate());let m=d/q,x=Math.round(i/q);i+=(m-x%m)%m*q}else i=Math.ceil(t/d)*d;let p=[];for(let s=i;s<=r;s+=d)s>=t&&p.push(s);return{ticks:p,step:d}}function Je(t,r){let u=new Date(t).toISOString();return r>=320*q?u.slice(0,4):r>=26*q?u.slice(0,7):r>=q?u.slice(5,10):r>=de?u.slice(5,16).replace("T"," "):u.slice(5,19).replace("T"," ")}function Qe(t,r={}){let u=t.getContext("2d");if(!u)throw new Error("chart: 2D context unavailable");let e=u,d=!!r.navigable,i=1,p=null,s=-1,m=0,x=0,f=1,k=0,z=1,S=0,A=1,ie="";function W(){i=Fe(t)}let N=()=>d?(Ke+Ie)*i:0,D=()=>t.width-(C.l+C.r)*i,T=()=>t.height-(C.t+C.b)*i-N(),E=()=>d?S:x,I=()=>d?A:f,P=()=>I()-E()||1,Y=()=>f-x||1,H=n=>C.l*i+(n-E())/P()*D(),G=n=>t.height-C.b*i-N()-(n-k)/(z-k||1)*T(),V=()=>t.height-Ie*i,K=()=>(Ie-it)*i,ne=()=>t.width-(C.l+C.r)*i,Q=n=>C.l*i+(n-x)/Y()*ne(),Ae=n=>V()+K()-(n-k)/(z-k||1)*K(),X=(n,o,a)=>Math.max(o,Math.min(a,n)),oe=()=>{let n=p==null?void 0:p.series,o=n?n.timestamps.length:0,a=o>1?Y()/(o-1):1e3;return Math.max(a*8,1e3)};function J(n,o){let a=o-n,c=oe();if(a<c){let l=(n+o)/2;n=l-c/2,o=l+c/2,a=c}a>=Y()&&(n=x,o=f),n<x&&(o+=x-n,n=x),o>f&&(n-=o-f,o=f),S=X(n,x,f),A=X(o,x,f),re()}let De=n=>{let o=(n-C.l*i)/(D()||1);return E()+X(o,0,1)*P()},se=n=>{let o=(n-C.l*i)/(ne()||1);return x+X(o,0,1)*Y()};function Pe(n){let o=p==null?void 0:p.series;if(!o||o.timestamps.length===0)return-1;let a=(n-C.l*i)/(D()||1),c=E()+a*P(),l=o.timestamps,h=0,y=l.length-1;for(;h<y;){let _=h+y>>1;l[_]<c?h=_+1:y=_}return h>0&&c-l[h-1]<l[h]-c&&(h-=1),h}function Le(n,o){let a=n.timestamps;if(x=a[0],f=a[a.length-1],d){let y=`${a.length}:${x}:${f}`;y!==ie?(ie=y,S=x,A=f):(S=X(S,x,f),A=X(Math.max(A,S+oe()),x,f))}let c=1/0,l=-1/0;for(let y of n.values)te(y)&&(y<c&&(c=y),y>l&&(l=y));for(let y of o)y.scored&&(te(y.lower)&&y.lower<c&&(c=y.lower),te(y.upper)&&y.upper>l&&(l=y.upper));(!te(c)||!te(l))&&(c=0,l=1),l<=c&&(l=c+1);let h=(l-c)*.06;k=c-h,z=l+h}function ae(n,o,a){let c=p.series.timestamps;ze(e,c,n,E(),I(),C.l*i,D(),H,G,o,a,i)}function ge(n,o){let a=[],c=-1;for(let l=Math.max(0,o);l<n.length;l++){let h=n[l];h.scored&&te(h.lower)&&te(h.upper)?c===-1&&(c=l):c!==-1&&(a.push([c,l-1]),c=-1)}return c!==-1&&a.push([c,n.length-1]),a}function Z(){if(m=0,!p||t.width===0||t.height===0)return;let{series:n,scored:o,params:a,alerts:c}=p;if(n.timestamps.length===0){e.fillStyle=O("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let l=O("--clay"),h=O("--st-anomaly"),y=O("--faint"),_=O("--muted");e.fillStyle=O("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*i}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let g=0;g<=4;g++){let v=k+(z-k)*g/4,w=G(v);e.strokeStyle=R(y,.1),e.lineWidth=1*i,e.beginPath(),e.moveTo(C.l*i,w),e.lineTo(t.width-C.r*i,w),e.stroke(),e.fillStyle=_,e.textAlign="right",e.fillText(Be(v),(C.l-8)*i,w)}e.textBaseline="top";let F=t.height-C.b*i-N();if(d){let g=Xe(E(),I(),7);for(let v of g.ticks){let w=H(v);e.strokeStyle=R(y,.1),e.lineWidth=1*i,e.beginPath(),e.moveTo(w,C.t*i),e.lineTo(w,F),e.stroke(),e.fillStyle=_,e.textAlign=w<(C.l+24)*i?"left":w>t.width-(C.r+24)*i?"right":"center",e.fillText(Je(v,g.step),w,F+7*i)}}else{let g=P();for(let v=0;v<=5;v++){let w=x+g*v/5,U=H(w);e.fillStyle=_,e.textAlign=v===0?"left":v===5?"right":"center",e.fillText(je(w,g),U,F+7*i)}}e.save(),e.beginPath(),e.rect(C.l*i,C.t*i,D(),T()),e.clip();let B=o.length,M=Math.min(Ve(n,a),B),L=M<B?n.timestamps[M]:void 0,j=ge(o,M);e.fillStyle=R(l,.13);for(let[g,v]of j){e.beginPath(),e.moveTo(H(o[g].timestamp),G(o[g].upper));for(let w=g+1;w<=v;w++)e.lineTo(H(o[w].timestamp),G(o[w].upper));for(let w=v;w>=g;w--)e.lineTo(H(o[w].timestamp),G(o[w].lower));e.closePath(),e.fill()}e.strokeStyle=R(l,.4),e.lineWidth=1*i;for(let[g,v]of j)for(let w of["upper","lower"]){e.beginPath();for(let U=g;U<=v;U++){let Ce=H(o[U].timestamp),Te=G(o[U][w]);U===g?e.moveTo(Ce,Te):e.lineTo(Ce,Te)}e.stroke()}e.strokeStyle=R(y,.55),e.lineWidth=1*i,e.setLineDash([3*i,3*i]);for(let[g,v]of j){e.beginPath();for(let w=g;w<=v;w++){let U=o[w].center;if(!te(U))continue;let Ce=H(o[w].timestamp),Te=G(U);w===g?e.moveTo(Ce,Te):e.lineTo(Ce,Te)}e.stroke()}if(e.setLineDash([]),a.smoothing!=="none"){ae(n.values,R(l,.28),1.25);let g=o.map(v=>v.processedValue);ae(g,l,1.6)}else ae(n.values,l,1.5);for(let g=M;g<B;g++){let v=o[g];if(!v.scored||!te(v.value))continue;let w=H(v.timestamp),U=G(v.value);v.isAnomaly?(e.fillStyle=R(h,.18),e.beginPath(),e.arc(w,U,6*i,0,Math.PI*2),e.fill(),e.fillStyle=h,e.beginPath(),e.arc(w,U,3*i,0,Math.PI*2),e.fill()):n.truthAnomaly[g]&&(e.strokeStyle=R(_,.7),e.lineWidth=1.25*i,e.beginPath(),e.arc(w,U,3.5*i,0,Math.PI*2),e.stroke())}L!==void 0&&Ue(e,t,C,i,H,L,"detection at full power \u2192"),s>=0&&s<o.length&&me(s,a.windowSize,o,n,y),c&&c.length&&Ge(e,t,C,i,H,c,g=>g==="anomaly"?O("--st-anomaly"):g==="recovery"?O("--st-recovery"):O("--st-nodata")),e.restore(),d&&ve(n,c,l,y,_)}function ve(n,o,a,c,l){let h=V(),y=h+K(),_=C.l*i,F=t.width-C.r*i,B=Xe(x,f,5);e.save(),e.beginPath(),e.rect(_,h,ne(),K()),e.clip(),e.strokeStyle=R(c,.1),e.lineWidth=1*i;for(let g of B.ticks){let v=Q(g);e.beginPath(),e.moveTo(v,h),e.lineTo(v,y),e.stroke()}ze(e,n.timestamps,n.values,x,f,_,ne(),Q,Ae,R(a,.7),1.1,i),e.restore();let M=Q(S),L=Q(A);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(_,h,M-_,K()),e.fillRect(L,h,F-L,K()),o&&o.length){let g=O("--st-anomaly");e.fillStyle=R(g,.85);for(let v of o){let w=Q(v.t);e.fillRect(w-1*i,h,2*i,K())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(M,h,L-M,K()),e.strokeStyle=a,e.lineWidth=1.5*i,e.strokeRect(M,h+1,L-M,K()-2),e.fillStyle=a;let j=h+K()/2-8*i;e.fillRect(M-2*i,j,4*i,16*i),e.fillRect(L-2*i,j,4*i,16*i),e.font=`${10*i}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=l;for(let g of B.ticks){let v=Q(g);e.strokeStyle=R(c,.25),e.lineWidth=1*i,e.beginPath(),e.moveTo(v,y),e.lineTo(v,y+3*i),e.stroke(),e.textAlign=v<(C.l+26)*i?"left":v>t.width-(C.r+26)*i?"right":"center",e.fillText(Je(g,B.step),v,y+5*i)}}function me(n,o,a,c,l){let h=c.timestamps,y=Math.max(0,n-o),_=n-1,F=C.t*i,B=T();if(_>=y){let j=c.intervalSeconds*1e3/P()*D()*.5,g=H(h[y])-j,v=H(h[_])+j;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(g,F,v-g,B),e.strokeStyle=R(l,.5),e.lineWidth=1*i,e.beginPath(),e.moveTo(g,F),e.lineTo(g,F+B),e.moveTo(v,F),e.lineTo(v,F+B),e.stroke()}let M=H(h[n]);e.strokeStyle=R(l,.85),e.lineWidth=1*i,e.setLineDash([2*i,2*i]),e.beginPath(),e.moveTo(M,F),e.lineTo(M,F+B),e.stroke(),e.setLineDash([]);let L=a[n];if(L.scored&&te(L.lower)&&te(L.upper)){let j=5*i;e.strokeStyle=R(O("--clay"),.85),e.lineWidth=1.5*i;for(let g of[L.lower,L.upper]){let v=G(g);e.beginPath(),e.moveTo(M-j,v),e.lineTo(M+j,v),e.stroke()}}if(te(L.value)){let j=G(L.value);e.fillStyle=O("--term-bg"),e.beginPath(),e.arc(M,j,4*i,0,Math.PI*2),e.fill(),e.strokeStyle=L.isAnomaly?O("--st-anomaly"):O("--clay"),e.lineWidth=2*i,e.beginPath(),e.arc(M,j,4*i,0,Math.PI*2),e.stroke()}}function re(){m===0&&(m=requestAnimationFrame(Z))}function fe(){var a;if(!r.onHover||!p)return;if(s<0){r.onHover(null);return}let n=(a=p.scored[s])!=null?a:null,o={index:s,point:n,windowStart:Math.max(0,s-p.params.windowSize),windowEnd:s-1};r.onHover(o)}let ee=null,$=null,le=n=>{let o=t.getBoundingClientRect();return{x:(n.clientX-o.left)*i,y:(n.clientY-o.top)*i}},xe=n=>d&&n>=V(),ye=n=>{let o=Q(S),a=Q(A),c=8*i;return Math.abs(n-o)<=c?"l":Math.abs(n-a)<=c?"r":n>o&&n<a?"move":"out"};function pe(n){if(!p)return;let{x:o,y:a}=le(n);if(ee||$)return;if(xe(a)){s!==-1&&(s=-1,fe(),re());let l=ye(o);t.style.cursor=l==="l"||l==="r"?"ew-resize":l==="move"?"grab":"pointer";return}d&&(t.style.cursor="grab");let c=Pe(o);c!==s&&(s=c,fe(),re())}function we(){s!==-1&&(s=-1,fe(),re())}function ke(n){if(!d||!p)return;let{x:o,y:a}=le(n);if(a>=V()){let c=ye(o);if(c==="l")$={type:"l",grab:0,vMin:S,vMax:A};else if(c==="r")$={type:"r",grab:0,vMin:S,vMax:A};else if(c==="move")$={type:"move",grab:se(o),vMin:S,vMax:A};else{let l=se(o),h=P();J(l-h/2,l+h/2),$={type:"move",grab:l,vMin:S,vMax:A}}t.style.cursor="grabbing",n.preventDefault()}else a>C.t*i&&a<V()-Ke*i&&(ee={x:o,vMin:S,vMax:A},t.style.cursor="grabbing",n.preventDefault())}function Ee(n){if(!p)return;let{x:o}=le(n);if(ee){let a=(o-ee.x)*(ee.vMax-ee.vMin)/(D()||1);J(ee.vMin-a,ee.vMax-a)}else if($){let a=se(o);$.type==="l"?J(Math.min(a,A-oe()),A):$.type==="r"?J(S,Math.max(a,S+oe())):J($.vMin+(a-$.grab),$.vMax+(a-$.grab))}}function he(){(ee||$)&&(ee=null,$=null,t.style.cursor="grab")}function be(n){if(!d||!p)return;n.preventDefault();let{x:o,y:a}=le(n),c=X(P()*Math.pow(1.0015,n.deltaY),oe(),Y());if(a>=V()){let l=se(o);J(l-c/2,l+c/2)}else{let l=De(o),h=(l-S)/(P()||1);J(l-h*c,l-h*c+c)}}function Se(){d&&J(x,f)}t.addEventListener("mousemove",pe),t.addEventListener("mouseleave",we),d&&(t.addEventListener("mousedown",ke),t.addEventListener("wheel",be,{passive:!1}),t.addEventListener("dblclick",Se),window.addEventListener("mousemove",Ee),window.addEventListener("mouseup",he));function Me(n){p=n,Le(n.series,n.scored),s>=n.series.timestamps.length&&(s=-1),re()}function Ne(){W(),p&&(Le(p.series,p.scored),re())}function _e(){t.removeEventListener("mousemove",pe),t.removeEventListener("mouseleave",we),d&&(t.removeEventListener("mousedown",ke),t.removeEventListener("wheel",be),t.removeEventListener("dblclick",Se),window.removeEventListener("mousemove",Ee),window.removeEventListener("mouseup",he)),m!==0&&cancelAnimationFrame(m),m=0,p=null}return W(),{render:Me,resize:Ne,destroy:_e}}var st={mad:3,zscore:3,iqr:1.5},at={mad:10,zscore:3,iqr:4},lt="dtk-tune";function b(t,r,u){let e=document.createElement(t);return r&&(e.className=r),u!=null&&(e.textContent=u),e}function He(t,r){let u=b("label","dtk-ctl-label",t);if(r){u.title=r;let e=b("span","dtk-ctl-info","\u24D8");e.title=r,u.appendChild(document.createTextNode(" ")),u.appendChild(e)}return u}function We(t,r,u,e,d){let i=b("div","dtk-ctl");i.appendChild(He(t,d));let p=b("div","dtk-seg"),s=u,m=[],x=()=>{m.forEach(f=>f.classList.toggle("on",f.dataset.v===s))};return r.forEach(f=>{let k=b("button","dtk-seg-btn",f.label);k.type="button",k.dataset.v=f.value,k.onclick=()=>{s=f.value,x(),e(s)},m.push(k),p.appendChild(k)}),x(),i.appendChild(p),{row:i,get:()=>s,set:f=>{s=f,x()}}}function Re(t,r,u){var x;let e=b("div","dtk-ctl"),d=b("div","dtk-ctl-head"),i=He(t,r.hint),p=b("span","dtk-ctl-val"),s=(x=r.fmt)!=null?x:(f=>String(f));d.appendChild(i),d.appendChild(p),e.appendChild(d);let m=b("input","dtk-range");return m.type="range",m.min=String(r.min),m.max=String(r.max),m.step=String(r.step),m.value=String(r.value),p.textContent=s(r.value),m.oninput=()=>{let f=Number(m.value);p.textContent=s(f),u(f)},e.appendChild(m),{row:e,get:()=>Number(m.value),setMax:f=>{m.max=String(f),Number(m.value)>f&&(m.value=String(f),p.textContent=s(f))}}}function et(t){let r={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(r.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(r.half_life=t.halfLife)),t.detrend!=="none"&&(r.detrend=t.detrend),t.smoothing!=="none"&&(r.smoothing=t.smoothing),t.inputType!=="values"&&(r.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(r.seasonality_components=t.seasonalityComponents,r.min_samples_per_group=t.minSamplesPerGroup),r}function ct(t,r){let u=et(t),e=[`type: ${t.type}`];for(let[d,i]of Object.entries(u))e.push(`${d}=${typeof i=="object"?JSON.stringify(i):i}`);return e.push(`consecutive_anomalies=${r}`),e.join(" \xB7 ")}function ut(t,r){var Ne,_e;pt(),r.classList.add(lt),r.innerHTML="";let u=b("div","dtk-tune-root");r.appendChild(u);let e=t.points.length,d={timestamps:t.points.map(n=>n.t),values:t.points.map(n=>n.v==null?NaN:n.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},i=d,p=n=>{let o=Math.max(0,e-n);return o<=0?d:{timestamps:d.timestamps.slice(o),values:d.values.slice(o),intervalSeconds:d.intervalSeconds,truthAnomaly:d.truthAnomaly.slice(o),seasonalityData:d.seasonalityData?d.seasonalityData.slice(o):void 0,seasonalityColumns:d.seasonalityColumns}},s=t.detector,m=t.consecutive_anomalies,x=(Ne=s.seasonalityComponents)!=null?Ne:[],f=new Map;x.forEach((n,o)=>n.forEach(a=>f.set(a,o+1)));let k=()=>{let n=0;f.forEach(a=>{a>n&&(n=a)});let o=[];for(let a=1;a<=n;a++){let c=t.seasonality_columns.filter(l=>f.get(l)===a);c.length&&o.push(c)}return o.length?o:null},z=()=>{var n;return{type:ve.get(),threshold:me.get(),windowSize:le.get(),minSamples:s.minSamples,inputType:s.inputType,smoothing:ke.get(),smoothingAlpha:s.smoothingAlpha,smoothingWindow:s.smoothingWindow,windowWeights:xe.get(),halfLife:xe.get()==="exponential"?ye.get():null,detrend:we.get(),seasonalityComponents:k(),minSamplesPerGroup:(n=at[ve.get()])!=null?n:s.minSamplesPerGroup,consecutiveAnomalies:m}},S=b("div","dtk-tune-header"),A=b("div","dtk-tune-titlerow");A.appendChild(b("h1","dtk-tune-title",t.metric));let ie=b("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");A.appendChild(ie),S.appendChild(A);let W=t.project?`${t.project} \xB7 `:"";S.appendChild(b("div","dtk-tune-sub",`${W}${e} points \xB7 ${ft(t.interval_seconds)} grid`)),t.description&&S.appendChild(b("div","dtk-tune-desc",t.description)),u.appendChild(S);let N=b("div","dtk-tune-grid"),D=b("div","dtk-tune-controls"),T=b("div","dtk-tune-main");N.appendChild(D),N.appendChild(T),u.appendChild(N);let E=b("div","dtk-tune-trim"),I=b("div","dtk-tune-trim-head");I.appendChild(He("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 P=b("span","dtk-tune-trim-val");I.appendChild(P),E.appendChild(I);let Y=b("input","dtk-range");Y.type="range",Y.min=String(Math.min(e,200)),Y.max=String(e),Y.step=String(Math.max(1,Math.round(e/200))),Y.value=String(e),E.appendChild(Y),T.appendChild(E);let H=b("div","dtk-tune-chart"),G=b("canvas");H.appendChild(G);let V=b("div","dtk-tune-spin");V.appendChild(b("span","dtk-spin-ring")),V.appendChild(b("span","dtk-spin-txt","computing\u2026")),H.appendChild(V),T.appendChild(H);let K=b("div","dtk-tune-legend"),ne=(n,o,a)=>{let c=b("span","dtk-leg-item");c.title=a,c.appendChild(b("span",`dtk-leg-sw ${n}`)),c.appendChild(b("span","dtk-leg-txt",o)),K.appendChild(c)};ne("line","metric","The metric value over time."),ne("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),ne("center","band center","The expected value at the middle of the band."),ne("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),ne("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),T.appendChild(K);let Q=b("div","dtk-tune-readout");T.appendChild(Q);let Ae=Qe(G,{navigable:!0,onHover:n=>{if(!n||!n.point||!n.point.scored){Q.textContent="";return}let o=n.point;Q.textContent=`t=${dt(o.timestamp)} value=${qe(o.value)} band=[${qe(o.lower)}, ${qe(o.upper)}]`+(o.isAnomaly?` \u26A0 ${o.direction} (sev ${o.severity.toFixed(2)})`:"")}}),X=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function T(n){let e=0;for(let t of n)e+=t;return e}function ie(n){let e=n.map((t,r)=>r);return e.sort((t,r)=>n[t]<n[r]?-1:n[t]>n[r]?1:t-r),e}function se(n,e,t){let r=e.length;if(r===0)return NaN;if(n<=e[0])return t[0];if(n>=e[r-1])return t[r-1];for(let i=1;i<r;i++)if(n<=e[i]){let o=e[i-1],c=e[i],u=t[i-1],s=t[i];if(c===o)return u;let d=(n-o)/(c-o);return u+d*(s-u)}return t[r-1]}function F(n,e,t){let r=ie(n),i=r.map(b=>n[b]),o=r.map(b=>e[b]),c=T(o),u=new Array(o.length),s=0;for(let b=0;b<o.length;b++)s+=o[b],u[b]=(s-.5*o[b])/c;let d=t/100;return se(d,u,i)}function S(n,e){return F(n,e,50)}function ce(n,e,t){let r=t===void 0?S(n,e):t,i=n.map(o=>Math.abs(o-r));return S(i,e)}function ue(n,e){let t=T(e),r=0;for(let i=0;i<n.length;i++)r+=n[i]*(e[i]/t);return r}function me(n,e,t,r=0){let i=T(e),o=e.map(s=>s/i),c=t===void 0?T(n.map((s,d)=>s*o[d])):t,u=0;for(let s=0;s<n.length;s++)u+=o[s]*(n[s]-c)**2;if(r===1){let s=1-T(o.map(d=>d*d));s>1e-12&&(u/=s)}return Math.sqrt(u)}function le(n,e){return e.smoothing==="none"?n.slice():e.smoothing==="ema"?ae(n,e.smoothingAlpha):fe(n,e.smoothingWindow)}function ae(n,e){let t=n.length,r=new Array(t).fill(NaN);if(t===0)return r;let i=-1;for(let o=0;o<t;o++)if(!Number.isNaN(n[o])){i=o;break}if(i===-1)return r;r[i]=n[i];for(let o=i+1;o<t;o++)Number.isNaN(n[o])?r[o]=r[o-1]:r[o]=e*n[o]+(1-e)*r[o-1];return r}function fe(n,e){let t=n.length,r=new Array(t).fill(NaN);for(let i=0;i<t;i++){let o=Math.max(0,i-e+1),c=0,u=0;for(let s=o;s<=i;s++)Number.isNaN(n[s])||(c+=n[s],u+=1);r[i]=u>0?c/u:NaN}return r}function de(n,e){let t=n.length;if(e.inputType==="values")return n.slice();let r=new Array(t).fill(NaN);for(let i=1;i<t;i++){let o=n[i-1],c=n[i];e.inputType==="changes"?r[i]=(c-o)/o:e.inputType==="absolute_changes"?r[i]=c-o:r[i]=Math.log(c+1)-Math.log(o+1)}return r}function be(n,e){return n.halfLife!==null?n.halfLife:Math.max(n.windowSize/20,e/2,1)}function pe(n,e){if(n.windowWeights==="none")return null;let t=n.windowSize,r=new Array(t);if(n.windowWeights==="exponential"){let i=be(n,e);for(let o=1;o<=t;o++){let c=Math.min(o/i,1e3);r[o-1]=Math.pow(.5,c)}return r}for(let i=1;i<=t;i++)r[i-1]=(t+1-i)/t;return r}function K(n,e){return e===null?n.map(()=>1):n.map(t=>e[t-1])}function ge(n){let e=n[0];for(let t of n)t>e&&(e=t);return e}function ye(n){let e=n[0];for(let t of n)t<e&&(e=t);return e}function he(n,e,t){if(n.length<4)return 0;let r=(ge(e)+ye(e))/2,i=e.map(l=>l>r),o=i.map(l=>!l),c=0,u=0;for(let l=0;l<e.length;l++)i[l]?c++:u++;if(c<2||u<2)return 0;let s=(l,N)=>N.filter((L,y)=>l[y]),d=s(o,n),b=s(i,n),k=s(o,e),C=s(i,e),g=s(o,t),f=s(i,t),q=S(d,g),p=S(b,f),w=S(k,g),M=S(C,f);return M===w?0:(q-p)/(M-w)}var Se={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function Q(n,e,t){if(n==="mad"){let o=S(e,t);return{median:o,mad:ce(e,t,o)}}if(n==="zscore"){let o=ue(e,t);return{mean:o,std:me(e,t,o,1)}}let r=F(e,t,25),i=F(e,t,75);return{q1:r,q3:i,iqr:i-r}}function we(n,e,t){if(n==="mad"){if(e.mad===0)return[e.median-1e-10,e.median+1e-10];let r=t*1.4826*e.mad;return[e.median-r,e.median+r]}return n==="zscore"?e.std===0?[e.mean-1e-10,e.mean+1e-10]:[e.mean-t*e.std,e.mean+t*e.std]:e.iqr===0?[e.q1-1e-10,e.q3+1e-10]:[e.q1-t*e.iqr,e.q3+t*e.iqr]}function Me(n,e,t){if(n==="mad"){let r=1.4826*e.mad;return r>0?t/r:1/0}return n==="zscore"?e.std>0?t/e.std:1/0:e.iqr>0?t/e.iqr:1/0}function Ne(n,e){return n==="mad"?e.median:n==="zscore"?e.mean:(e.q1+e.q3)/2}function U(n,e,t,r,i){return{index:n,timestamp:e,value:t,processedValue:r,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Y(n,e){let{type:t}=e,r=Math.max(e.minSamples,X[t]),i=Se[t],{timestamps:o,values:c}=n,u=o.length,s=le(c,e),d=de(s,e),b=e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0,k=n.seasonalityData,C=pe(e,r),g=[];for(let f=0;f<u;f++){let q=c[f],p=d[f],w=o[f];if(Number.isNaN(p)){g.push(U(f,w,q,p,"missing_data"));continue}let M=Math.max(0,f-e.windowSize),l=f-M,N=d.slice(M,f),L=N.map(m=>!Number.isNaN(m)),y=[];for(let m=0;m<N.length;m++)L[m]&&y.push(N[m]);if(y.length<r){g.push(U(f,w,q,p,"insufficient_data"));continue}let O=new Array(l);for(let m=0;m<l;m++)O[m]=l-m;let z=[];for(let m=0;m<l;m++)L[m]&&z.push(O[m]);let V=K(z,C),P=0;e.detrend==="linear"&&(P=he(y,z,V));let $=P!==0?y.map((m,W)=>m+P*z[W]):y,G=Q(t,$,V),I={...G};if(b&&k){let m=k[f];for(let W of e.seasonalityComponents){let J=new Array(l).fill(!0);for(let a=0;a<l;a++){let D=k[M+a],x=!0;for(let j of W)if(!D||D[j]!==(m==null?void 0:m[j])){x=!1;break}J[a]=x}let _=[],E=[];for(let a=0;a<l;a++)L[a]&&J[a]&&(_.push(N[a]),E.push(O[a]));if(_.length<e.minSamplesPerGroup)continue;let ne=K(E,C),te=P!==0?_.map((a,D)=>a+P*E[D]):_,re=Q(t,te,ne);for(let[a,D]of i){let x=G[a],oe=(D==="spread"?x>0:x!==0)?re[a]/x:1;I[a]*=oe}}}let[h,A]=we(t,I,e.threshold);h>A&&([h,A]=[A,h]);let H=p<h||p>A,R=null,B=0;if(H){let m;p<h?(R="below",m=h-p):(R="above",m=p-A),B=Me(t,I,m)}let ee=Ne(t,I);g.push({index:f,timestamp:w,value:q,processedValue:p,scored:!0,isAnomaly:H,lower:h,upper:A,center:ee,direction:R,severity:B,reason:"ok"})}return g}function Ae(n,e){let t=n.seasonalityData;if(!t||t.length===0)return 0;let r=0;for(let i of e){let o=new Set;for(let c of t)o.add(i.map(u=>{var s;return String((s=c==null?void 0:c[u])!=null?s:"")}).join("|"));r=Math.max(r,o.size)}return r}function Z(n,e){let t=n.timestamps.length,r=Math.max(e.minSamples,X[e.type]);if(e.smoothing==="sma"&&(r=Math.max(r,e.smoothingWindow-1)),e.smoothing==="ema"&&(r=Math.max(r,Math.ceil(5/e.smoothingAlpha))),e.inputType!=="values"&&(r=Math.max(r,1)),e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0){let o=Ae(n,e.seasonalityComponents);if(o>0){let c=e.minSamplesPerGroup*o;e.windowSize>=c&&(r=Math.max(r,c))}}return Math.min(r,t)}var v=null;function De(n,e,t){let r=[],i=0;for(let o=0;o<n.length;o++){if(!(n[o].scored&&n[o].isAnomaly)){i=0;continue}let u=o>0&&n[o-1].scored&&n[o-1].isAnomaly,s=o>0&&n[o].timestamp-n[o-1].timestamp===e;i=u&&s?i+1:1,i===t&&r.push(o)}return r}self.onmessage=n=>{let e=n.data;if(e.type==="series"){v=e.series;return}if(e.type==="run"&&v){let t=e.params,r=Y(v,t),i=v.intervalSeconds*1e3,o=De(r,i,t.consecutiveAnomalies),c=Z(v,t),u=0;for(let s of r)s.scored&&s.isAnomaly&&u++;self.postMessage({type:"result",id:e.id,scored:r,fires:o,eff:c,flagged:u})}};})();
2
+ `],{type:"text/javascript"})));X.postMessage({type:"series",series:i});let oe=0,J=null;X.onmessage=n=>{let o=n.data;if(o.type!=="result"||o.id!==oe||!J)return;V.classList.remove("on");let a=J,c=o.fires.map(l=>({t:i.timestamps[l],kind:"anomaly"}));Ae.render({series:i,scored:o.scored,params:a,alerts:c}),he.textContent=`${o.flagged} flagged \xB7 ${o.fires.length} alert${o.fires.length===1?"":"s"} \xB7 warm-up ${o.eff} pts`,Se.textContent=ct(a,m)},X.onerror=()=>{V.classList.remove("on"),he.textContent="recompute failed \u2014 see the browser console"};let De=()=>{J=z(),oe+=1,V.classList.add("on"),X.postMessage({type:"run",id:oe,params:J})},se=n=>n>1?mt(d.timestamps[e-1]-d.timestamps[e-n]):"\u2014",Pe=n=>{P.textContent=n>=e?`${e} pts \xB7 full (${se(e)})`:`${n} pts \xB7 ${se(n)}`};Pe(e);function Le(n){i=p(n),X.postMessage({type:"series",series:i}),Z()}let ae=0;Y.oninput=()=>{let n=Number(Y.value);Pe(n),ae&&window.clearTimeout(ae),ae=window.setTimeout(()=>Le(n),200)};let ge=0,Z=()=>{ge&&window.clearTimeout(ge),ge=window.setTimeout(De,130)},ve=We("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"}],s.type,n=>{var o;ee((o=st[n])!=null?o:3),Z()},"The statistic used for the center/spread of the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles). All share the same windowing.");D.appendChild(ve.row);let me=Re("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:s.threshold,fmt:n=>n.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},Z);D.appendChild(me.row);let re=me.row.querySelector("input"),fe=me.row.querySelector(".dtk-ctl-val"),ee=n=>{re&&(re.value=String(n)),fe&&(fe.textContent=n.toFixed(1))},$=Math.max(50,Math.min(2e3,Math.floor(e/2))),le=Re("Window size (points)",{min:10,max:$,step:5,value:Math.min(s.windowSize,$),fmt:n=>String(n),hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},Z);D.appendChild(le.row);let xe=We("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],s.windowWeights,n=>{pe.style.display=n==="exponential"?"":"none",Z()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");D.appendChild(xe.row);let ye=Re("Half-life (points)",{min:1,max:$,step:1,value:(_e=s.halfLife)!=null?_e:Math.max(5,Math.round(s.windowSize/20)),fmt:n=>String(n),hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},Z),pe=ye.row;pe.style.display=s.windowWeights==="exponential"?"":"none",D.appendChild(pe);let we=We("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],s.detrend,Z,"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.");D.appendChild(we.row);let ke=We("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],s.smoothing,Z,"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.");if(D.appendChild(ke.row),t.seasonality_columns.length){let n=t.seasonality_columns,o=b("div","dtk-ctl");o.appendChild(He("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 a=Math.min(n.length,6),c=[{label:"\u2014",value:"0"}];for(let l=1;l<=a;l++)c.push({label:`G${l}`,value:String(l)});n.forEach(l=>{let h=b("div","dtk-season-row");h.appendChild(b("span","dtk-season-col",l));let y=b("div","dtk-seg dtk-season-seg"),_=[],F=()=>{var M;return(M=f.get(l))!=null?M:0},B=()=>_.forEach(M=>M.classList.toggle("on",Number(M.dataset.v)===F()));c.forEach(M=>{let L=b("button","dtk-seg-btn",M.label);L.type="button",L.dataset.v=M.value,L.title=M.value==="0"?`ignore ${l}`:`put ${l} in group ${M.value}`,L.onclick=()=>{f.set(l,Number(M.value)),B(),Z()},_.push(L),y.appendChild(L)}),B(),h.appendChild(y),o.appendChild(h)}),D.appendChild(o)}let Ee=Re("Alert: consecutive anomalies",{min:1,max:10,step:1,value:m,fmt:n=>String(n),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},n=>{m=n,Z()});D.appendChild(Ee.row);let he=b("div","dtk-tune-stat");T.appendChild(he);let be=b("div","dtk-tune-cfg");be.appendChild(b("span","dtk-tune-cfg-k","// effective config"));let Se=b("code","dtk-tune-cfg-v");if(be.appendChild(Se),T.appendChild(be),t.save_url){let n=b("div","dtk-tune-apply"),o=b("button","dtk-apply-btn","Apply to metric");o.type="button",o.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 a=b("span","dtk-apply-msg");o.onclick=()=>{let c=z();o.disabled=!0,a.className="dtk-apply-msg info",a.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:c.type,params:et(c)},consecutive_anomalies:m})}).then(l=>l.ok?l.json():l.text().then(h=>{throw new Error(h||`HTTP ${l.status}`)})).then(l=>{var h;a.className="dtk-apply-msg ok",a.textContent=`Applied \u2192 ${(h=l.saved)!=null?h:"metric"} (previous archived). You can close this tab.`}).catch(l=>{o.disabled=!1,a.className="dtk-apply-msg err",a.textContent=`Apply failed: ${l.message}`})},n.appendChild(o),n.appendChild(a),T.appendChild(n)}else T.appendChild(b("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."));De();let Me=0;window.addEventListener("resize",()=>{Me&&cancelAnimationFrame(Me),Me=requestAnimationFrame(()=>Ae.resize())})}function qe(t){if(!Number.isFinite(t))return"\u2014";let r=Math.abs(t);return r!==0&&(r<.01||r>=1e6)?t.toExponential(2):Number(t.toFixed(r<1?4:2)).toString()}function dt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function mt(t){let r=Math.round(t/6e4);if(r<60)return`${r}m`;let u=Math.floor(r/60);if(u<24)return`${u}h`;let e=Math.floor(u/24),d=u%24;return d?`${e}d ${d}h`:`${e}d`}function ft(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var Ze=!1;function pt(){if(Ze)return;Ze=!0;let t=`
3
+ .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
4
+ --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
5
+ --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
6
+ --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
7
+ .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
8
+ .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
9
+ .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
10
+ .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
11
+ color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
12
+ .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
13
+ .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
14
+ .dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
15
+ @media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
16
+ .dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
17
+ border:1px solid var(--border);border-radius:12px;padding:16px;}
18
+ .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
19
+ .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
20
+ .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
21
+ .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
22
+ .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
23
+ .dtk-seg.dtk-wrap{flex-wrap:wrap;}
24
+ .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
25
+ font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
26
+ .dtk-seg-btn:hover{color:var(--ink);}
27
+ .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
28
+ .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
29
+ .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
30
+ .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
31
+ .dtk-tune-chart{position:relative;width:100%;height:470px;background:var(--surface);
32
+ border:1px solid var(--border);border-radius:12px;overflow:hidden;}
33
+ .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
34
+ .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
35
+ .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
36
+ .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
37
+ font-size:12px;overflow-x:auto;}
38
+ .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
39
+ .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
40
+ .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
41
+ .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
42
+ font-size:14px;font-weight:600;cursor:pointer;}
43
+ .dtk-apply-btn:hover{background:var(--c7);}
44
+ .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
45
+ .dtk-apply-msg{font-size:13px;}
46
+ .dtk-apply-msg.ok{color:var(--green);}
47
+ .dtk-apply-msg.err{color:var(--anom);}
48
+ .dtk-apply-msg.info{color:var(--muted);}
49
+ .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
50
+ border-radius:8px;padding:10px 12px;}
51
+ .dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
52
+ .dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
53
+ border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
54
+ .dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
55
+ .dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
56
+ .dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
57
+ background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
58
+ padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
59
+ .dtk-tune-spin.on{display:inline-flex;}
60
+ .dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
61
+ border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
62
+ @keyframes dtk-spin{to{transform:rotate(360deg);}}
63
+ .dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
64
+ .dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
65
+ .dtk-leg-sw{display:inline-block;flex:0 0 auto;}
66
+ .dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
67
+ .dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
68
+ border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
69
+ .dtk-leg-sw.center{width:16px;height:2px;
70
+ background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
71
+ .dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
72
+ .dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
73
+ border-top:7px solid var(--anom);}
74
+ .dtk-leg-txt{white-space:nowrap;}
75
+ .dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
76
+ .dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
77
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
78
+ .dtk-season-seg{flex:0 0 auto;padding:2px;}
79
+ .dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
80
+ `,r=document.createElement("style");r.textContent=t,document.head.appendChild(r)}window.__DTK_TUNE__={render:ut};})();
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.30.1
3
+ Version: 0.31.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/alexeiveselov92/detectkit
8
- Project-URL: Documentation, https://github.com/alexeiveselov92/detectkit
7
+ Project-URL: Homepage, https://dtk.pipelab.dev
8
+ Project-URL: Documentation, https://dtk.pipelab.dev
9
9
  Project-URL: Repository, https://github.com/alexeiveselov92/detectkit
10
+ Project-URL: Changelog, https://github.com/alexeiveselov92/detectkit/blob/main/CHANGELOG.md
10
11
  Project-URL: Issues, https://github.com/alexeiveselov92/detectkit/issues
11
12
  Keywords: monitoring,anomaly-detection,metrics,timeseries,alerting
12
13
  Classifier: Development Status :: 3 - Alpha
@@ -66,14 +67,22 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
66
67
  Requires-Dist: pymysql>=1.0.0; extra == "integration"
67
68
  Dynamic: license-file
68
69
 
69
- # detectkit
70
+ <a href="https://dtk.pipelab.dev">
71
+ <picture>
72
+ <source media="(prefers-color-scheme: dark)" srcset="assets/detectkit-lockup-dark.svg">
73
+ <img src="assets/detectkit-lockup.svg" alt="detectkit" width="260">
74
+ </picture>
75
+ </a>
70
76
 
71
77
  [![PyPI version](https://img.shields.io/pypi/v/detectkit.svg)](https://pypi.org/project/detectkit/)
72
78
  [![Python](https://img.shields.io/pypi/pyversions/detectkit.svg)](https://pypi.org/project/detectkit/)
79
+ [![Docs & playground](https://img.shields.io/badge/docs-dtk.pipelab.dev-d15b36)](https://dtk.pipelab.dev)
73
80
 
74
81
  **Metric monitoring with automatic anomaly detection.**
75
82
 
76
- `detectkit` is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
83
+ [`detectkit`](https://dtk.pipelab.dev) is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
84
+
85
+ **Website, docs & live playground → [dtk.pipelab.dev](https://dtk.pipelab.dev)**
77
86
 
78
87
  ## Features
79
88
 
@@ -181,6 +190,7 @@ for r in results:
181
190
 
182
191
  ## Documentation
183
192
 
193
+ - **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
184
194
  - [Getting Started](docs/getting-started/quickstart.md) — 5-minute quickstart
185
195
  - [Configuration Guide](docs/guides/configuration.md) — all config options
186
196
  - [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
@@ -78,9 +78,10 @@ integration = [
78
78
  dtk = "detectkit.cli.main:cli"
79
79
 
80
80
  [project.urls]
81
- Homepage = "https://github.com/alexeiveselov92/detectkit"
82
- Documentation = "https://github.com/alexeiveselov92/detectkit"
81
+ Homepage = "https://dtk.pipelab.dev"
82
+ Documentation = "https://dtk.pipelab.dev"
83
83
  Repository = "https://github.com/alexeiveselov92/detectkit"
84
+ Changelog = "https://github.com/alexeiveselov92/detectkit/blob/main/CHANGELOG.md"
84
85
  Issues = "https://github.com/alexeiveselov92/detectkit/issues"
85
86
 
86
87
  [tool.setuptools.dynamic]
@@ -1,51 +0,0 @@
1
- "use strict";(()=>{var xe={"--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 z(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||xe[e]||"#888"}function we(e){let n=e.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let l=parseInt(n,16);return n.length!==6||Number.isNaN(l)?[209,91,54]:[l>>16&255,l>>8&255,l&255]}function F(e,n){let[l,t,o]=we(e);return`rgba(${l},${t},${o},${n})`}function ce(e){let n=Math.max(1,window.devicePixelRatio||1),l=e.clientWidth||e.offsetWidth||0,t=e.clientHeight||e.offsetHeight||0;return e.width=Math.round(l*n),e.height=Math.round(t*n),n}var le=Number.isFinite;function ue(e,n,l,t,o,r,a,c,s,C,w,N){let $=n.length,D=Math.max(1,Math.round(a)),Q=o-t||1,U=0;for(let v=0;v<$;v++){let h=l[v];!le(h)||n[v]<t||n[v]>o||U++}if(e.strokeStyle=C,e.lineWidth=w*N,e.lineJoin="round",e.beginPath(),U<=D){let v=!1;for(let h=0;h<$;h++){let E=l[h],M=n[h];if(!le(E)||M<t||M>o){v=!1;continue}let A=c(M),H=s(E);v?e.lineTo(A,H):(e.moveTo(A,H),v=!0)}}else{let v=new Array(D).fill(null),h=new Array(D).fill(null);for(let M=0;M<$;M++){let A=l[M],H=n[M];if(!le(A)||H<t||H>o)continue;let P=Math.floor((H-t)/Q*(D-1));P=P<0?0:P>D-1?D-1:P,(v[P]===null||A<v[P])&&(v[P]=A),(h[P]===null||A>h[P])&&(h[P]=A)}let E=!1;for(let M=0;M<D;M++){if(h[M]===null){E=!1;continue}let A=r+M,H=s(h[M]),P=s(v[M]);E?e.lineTo(A,H):(e.moveTo(A,H),E=!0),e.lineTo(A,P)}}e.stroke()}function me(e){let n=Math.abs(e);return n>=1e3?e.toFixed(0):n>=10?e.toFixed(1):n>=1?e.toFixed(2):e.toFixed(3)}function de(e,n){let l=new Date(e).toISOString();return n<2*864e5?l.slice(5,16).replace("T"," "):l.slice(5,10)}function fe(e,n,l){return{left:n.l*l,top:n.t*l,right:e.width-n.r*l,bottom:e.height-n.b*l}}function pe(e,n,l,t,o,r,a){let c=fe(n,l,t),s=Math.max(c.left,Math.min(o(r),c.right));s<=c.left+.5||(e.save(),e.fillStyle="rgba(17,15,13,0.42)",e.fillRect(c.left,c.top,s-c.left,c.bottom-c.top),e.strokeStyle=F(z("--faint"),.7),e.lineWidth=1*t,e.setLineDash([4*t,4*t]),e.beginPath(),e.moveTo(s,c.top),e.lineTo(s,c.bottom),e.stroke(),e.setLineDash([]),e.fillStyle=F(z("--faint"),.95),e.font=`${10*t}px ui-monospace, monospace`,e.textAlign="left",e.textBaseline="top",e.fillText(a,s+6*t,c.top+5*t),e.restore())}function be(e,n,l,t,o,r,a){let c=fe(n,l,t),s=5*t;e.save();for(let C=0;C<r.length;C++){let w=r[C],N=o(w.t);if(N<c.left-1||N>c.right+1)continue;let $=a(w.kind);e.strokeStyle=F($,.45),e.lineWidth=1*t,e.beginPath(),e.moveTo(N,c.top),e.lineTo(N,c.bottom),e.stroke(),e.fillStyle=$,e.beginPath(),e.moveTo(N-s,c.top),e.lineTo(N+s,c.top),e.lineTo(N,c.top+s*1.4),e.closePath(),e.fill()}e.restore()}var ke={mad:1,zscore:2,iqr:4};function Se(e,n){let l=e.seasonalityData;if(!l||l.length===0)return 0;let t=0;for(let o of n){let r=new Set;for(let a of l)r.add(o.map(c=>{var s;return String((s=a==null?void 0:a[c])!=null?s:"")}).join("|"));t=Math.max(t,r.size)}return t}function he(e,n){let l=e.timestamps.length,t=Math.max(n.minSamples,ke[n.type]);if(n.smoothing==="sma"&&(t=Math.max(t,n.smoothingWindow-1)),n.smoothing==="ema"&&(t=Math.max(t,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(t=Math.max(t,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Se(e,n.seasonalityComponents);if(r>0){let a=n.minSamplesPerGroup*r;n.windowSize>=a&&(t=Math.max(t,a))}}return Math.min(t,l)}var L={l:52,r:14,t:14,b:26},G=Number.isFinite;function ge(e,n={}){let l=e.getContext("2d");if(!l)throw new Error("chart: 2D context unavailable");let t=l,o=1,r=null,a=-1,c=0,s=0,C=1,w=0,N=1;function $(){o=ce(e)}let D=()=>e.width-(L.l+L.r)*o,Q=()=>e.height-(L.t+L.b)*o,U=()=>C-s||1,v=m=>L.l*o+(m-s)/U()*D(),h=m=>e.height-L.b*o-(m-w)/(N-w||1)*Q();function E(m){let b=r==null?void 0:r.series;if(!b||b.timestamps.length===0)return-1;let T=(m-L.l*o)/(D()||1),d=s+T*U(),g=b.timestamps,S=0,k=g.length-1;for(;S<k;){let O=S+k>>1;g[O]<d?S=O+1:k=O}return S>0&&d-g[S-1]<g[S]-d&&(S-=1),S}function M(m,b){let T=m.timestamps;s=T[0],C=T[T.length-1];let d=1/0,g=-1/0;for(let k of m.values)G(k)&&(k<d&&(d=k),k>g&&(g=k));for(let k of b)k.scored&&(G(k.lower)&&k.lower<d&&(d=k.lower),G(k.upper)&&k.upper>g&&(g=k.upper));(!G(d)||!G(g))&&(d=0,g=1),g<=d&&(g=d+1);let S=(g-d)*.06;w=d-S,N=g+S}function A(m,b,T){let d=r.series.timestamps;ue(t,d,m,s,C,L.l*o,D(),v,h,b,T,o)}function H(m,b){let T=[],d=-1;for(let g=Math.max(0,b);g<m.length;g++){let S=m[g];S.scored&&G(S.lower)&&G(S.upper)?d===-1&&(d=g):d!==-1&&(T.push([d,g-1]),d=-1)}return d!==-1&&T.push([d,m.length-1]),T}function P(){if(c=0,!r||e.width===0||e.height===0)return;let{series:m,scored:b,params:T,alerts:d}=r;if(m.timestamps.length===0){t.fillStyle=z("--term-bg"),t.fillRect(0,0,e.width,e.height);return}let g=z("--clay"),S=z("--st-anomaly"),k=z("--faint"),O=z("--muted");t.fillStyle=z("--term-bg"),t.fillRect(0,0,e.width,e.height),t.font=`${11*o}px ui-monospace, 'JetBrains Mono', monospace`,t.textBaseline="middle";for(let u=0;u<=4;u++){let y=w+(N-w)*u/4,x=h(y);t.strokeStyle=F(k,.1),t.lineWidth=1*o,t.beginPath(),t.moveTo(L.l*o,x),t.lineTo(e.width-L.r*o,x),t.stroke(),t.fillStyle=O,t.textAlign="right",t.fillText(me(y),(L.l-8)*o,x)}t.textBaseline="top";let j=U();for(let u=0;u<=5;u++){let y=s+j*u/5,x=v(y);t.fillStyle=O,t.textAlign=u===0?"left":u===5?"right":"center",t.fillText(de(y,j),x,(e.height-L.b+7)*o)}t.save(),t.beginPath(),t.rect(L.l*o,L.t*o,D(),Q()),t.clip();let X=b.length,B=Math.min(he(m,T),X),R=B<X?m.timestamps[B]:void 0,W=H(b,B);t.fillStyle=F(g,.13);for(let[u,y]of W){t.beginPath(),t.moveTo(v(b[u].timestamp),h(b[u].upper));for(let x=u+1;x<=y;x++)t.lineTo(v(b[x].timestamp),h(b[x].upper));for(let x=y;x>=u;x--)t.lineTo(v(b[x].timestamp),h(b[x].lower));t.closePath(),t.fill()}t.strokeStyle=F(g,.4),t.lineWidth=1*o;for(let[u,y]of W)for(let x of["upper","lower"]){t.beginPath();for(let I=u;I<=y;I++){let i=v(b[I].timestamp),f=h(b[I][x]);I===u?t.moveTo(i,f):t.lineTo(i,f)}t.stroke()}t.strokeStyle=F(k,.55),t.lineWidth=1*o,t.setLineDash([3*o,3*o]);for(let[u,y]of W){t.beginPath();for(let x=u;x<=y;x++){let I=b[x].center;if(!G(I))continue;let i=v(b[x].timestamp),f=h(I);x===u?t.moveTo(i,f):t.lineTo(i,f)}t.stroke()}if(t.setLineDash([]),T.smoothing!=="none"){A(m.values,F(g,.28),1.25);let u=b.map(y=>y.processedValue);A(u,g,1.6)}else A(m.values,g,1.5);for(let u=B;u<X;u++){let y=b[u];if(!y.scored||!G(y.value))continue;let x=v(y.timestamp),I=h(y.value);y.isAnomaly?(t.fillStyle=F(S,.18),t.beginPath(),t.arc(x,I,6*o,0,Math.PI*2),t.fill(),t.fillStyle=S,t.beginPath(),t.arc(x,I,3*o,0,Math.PI*2),t.fill()):m.truthAnomaly[u]&&(t.strokeStyle=F(O,.7),t.lineWidth=1.25*o,t.beginPath(),t.arc(x,I,3.5*o,0,Math.PI*2),t.stroke())}R!==void 0&&pe(t,e,L,o,v,R,"detection at full power \u2192"),a>=0&&a<b.length&&ee(a,T.windowSize,b,m,k),d&&d.length&&be(t,e,L,o,v,d,u=>u==="anomaly"?z("--st-anomaly"):u==="recovery"?z("--st-recovery"):z("--st-nodata")),t.restore()}function ee(m,b,T,d,g){let S=d.timestamps,k=Math.max(0,m-b),O=m-1,j=L.t*o,X=Q();if(O>=k){let W=d.intervalSeconds*1e3/U()*D()*.5,u=v(S[k])-W,y=v(S[O])+W;t.fillStyle="rgba(255,255,255,0.05)",t.fillRect(u,j,y-u,X),t.strokeStyle=F(g,.5),t.lineWidth=1*o,t.beginPath(),t.moveTo(u,j),t.lineTo(u,j+X),t.moveTo(y,j),t.lineTo(y,j+X),t.stroke()}let B=v(S[m]);t.strokeStyle=F(g,.85),t.lineWidth=1*o,t.setLineDash([2*o,2*o]),t.beginPath(),t.moveTo(B,j),t.lineTo(B,j+X),t.stroke(),t.setLineDash([]);let R=T[m];if(R.scored&&G(R.lower)&&G(R.upper)){let W=5*o;t.strokeStyle=F(z("--clay"),.85),t.lineWidth=1.5*o;for(let u of[R.lower,R.upper]){let y=h(u);t.beginPath(),t.moveTo(B-W,y),t.lineTo(B+W,y),t.stroke()}}if(G(R.value)){let W=h(R.value);t.fillStyle=z("--term-bg"),t.beginPath(),t.arc(B,W,4*o,0,Math.PI*2),t.fill(),t.strokeStyle=R.isAnomaly?z("--st-anomaly"):z("--clay"),t.lineWidth=2*o,t.beginPath(),t.arc(B,W,4*o,0,Math.PI*2),t.stroke()}}function J(){c===0&&(c=requestAnimationFrame(P))}function Z(){var T;if(!n.onHover||!r)return;if(a<0){n.onHover(null);return}let m=(T=r.scored[a])!=null?T:null,b={index:a,point:m,windowStart:Math.max(0,a-r.params.windowSize),windowEnd:a-1};n.onHover(b)}function oe(m){if(!r)return;let b=e.getBoundingClientRect(),T=(m.clientX-b.left)*o,d=E(T);d!==a&&(a=d,Z(),J())}function te(){a!==-1&&(a=-1,Z(),J())}e.addEventListener("mousemove",oe),e.addEventListener("mouseleave",te);function K(m){r=m,M(m.series,m.scored),a>=m.series.timestamps.length&&(a=-1),J()}function re(){$(),r&&(M(r.series,r.scored),J())}function ne(){e.removeEventListener("mousemove",oe),e.removeEventListener("mouseleave",te),c!==0&&cancelAnimationFrame(c),c=0,r=null}return $(),{render:K,resize:re,destroy:ne}}var Ce={mad:3,zscore:3,iqr:1.5},Te={mad:10,zscore:3,iqr:4},Me="dtk-tune";function p(e,n,l){let t=document.createElement(e);return n&&(t.className=n),l!=null&&(t.textContent=l),t}function ie(e,n,l,t){let o=p("div","dtk-ctl");o.appendChild(p("label","dtk-ctl-label",e));let r=p("div","dtk-seg"),a=l,c=[],s=()=>{c.forEach(C=>C.classList.toggle("on",C.dataset.v===a))};return n.forEach(C=>{let w=p("button","dtk-seg-btn",C.label);w.type="button",w.dataset.v=C.value,w.onclick=()=>{a=C.value,s(),t(a)},c.push(w),r.appendChild(w)}),s(),o.appendChild(r),{row:o,get:()=>a,set:C=>{a=C,s()}}}function se(e,n,l){var C;let t=p("div","dtk-ctl"),o=p("div","dtk-ctl-head"),r=p("label","dtk-ctl-label",e),a=p("span","dtk-ctl-val"),c=(C=n.fmt)!=null?C:(w=>String(w));o.appendChild(r),o.appendChild(a),t.appendChild(o);let s=p("input","dtk-range");return s.type="range",s.min=String(n.min),s.max=String(n.max),s.step=String(n.step),s.value=String(n.value),a.textContent=c(n.value),s.oninput=()=>{let w=Number(s.value);a.textContent=c(w),l(w)},t.appendChild(s),{row:t,get:()=>Number(s.value),setMax:w=>{s.max=String(w),Number(s.value)>w&&(s.value=String(w),a.textContent=c(w))}}}function ye(e){let n={threshold:e.threshold,window_size:e.windowSize};return e.windowWeights!=="none"&&(n.window_weights=e.windowWeights,e.windowWeights==="exponential"&&e.halfLife!=null&&(n.half_life=e.halfLife)),e.detrend!=="none"&&(n.detrend=e.detrend),e.smoothing!=="none"&&(n.smoothing=e.smoothing),e.inputType!=="values"&&(n.input_type=e.inputType),e.seasonalityComponents&&e.seasonalityComponents.length&&(n.seasonality_components=e.seasonalityComponents,n.min_samples_per_group=e.minSamplesPerGroup),n}function Ae(e,n){let l=ye(e),t=[`type: ${e.type}`];for(let[o,r]of Object.entries(l))t.push(`${o}=${typeof r=="object"?JSON.stringify(r):r}`);return t.push(`consecutive_anomalies=${n}`),t.join(" \xB7 ")}function De(e,n){var x,I;Le(),n.classList.add(Me),n.innerHTML="";let l=p("div","dtk-tune-root");n.appendChild(l);let t=e.points.length,o={timestamps:e.points.map(i=>i.t),values:e.points.map(i=>i.v==null?NaN:i.v),intervalSeconds:e.interval_seconds,truthAnomaly:new Array(t).fill(!1),seasonalityData:e.seasonality_columns.length?e.seasonality:void 0,seasonalityColumns:e.seasonality_columns.length?e.seasonality_columns:void 0},r=e.detector,a=e.consecutive_anomalies,c=(x=r.seasonalityComponents)!=null?x:[],s=new Set(c.flat()),C=c.length===1&&c[0].length>1,w=()=>{let i=e.seasonality_columns.filter(f=>s.has(f));return i.length?C?[i]:i.map(f=>[f]):null},N=()=>{var i;return{type:re.get(),threshold:ne.get(),windowSize:g.get(),minSamples:r.minSamples,inputType:r.inputType,smoothing:X.get(),smoothingAlpha:r.smoothingAlpha,smoothingWindow:r.smoothingWindow,windowWeights:S.get(),halfLife:S.get()==="exponential"?k.get():null,detrend:j.get(),seasonalityComponents:w(),minSamplesPerGroup:(i=Te[re.get()])!=null?i:r.minSamplesPerGroup,consecutiveAnomalies:a}},$=p("div","dtk-tune-header"),D=p("div","dtk-tune-titlerow");D.appendChild(p("h1","dtk-tune-title",e.metric));let Q=p("span","dtk-tune-badge",e.save_url?"manual tuning":"preview");D.appendChild(Q),$.appendChild(D);let U=e.project?`${e.project} \xB7 `:"";$.appendChild(p("div","dtk-tune-sub",`${U}${t} points \xB7 ${_e(e.interval_seconds)} grid`)),e.description&&$.appendChild(p("div","dtk-tune-desc",e.description)),l.appendChild($);let v=p("div","dtk-tune-grid"),h=p("div","dtk-tune-controls"),E=p("div","dtk-tune-main");v.appendChild(h),v.appendChild(E),l.appendChild(v);let M=p("div","dtk-tune-chart"),A=p("canvas");M.appendChild(A),E.appendChild(M);let H=p("div","dtk-tune-readout");E.appendChild(H);let P=ge(A,{onHover:i=>{if(!i||!i.point||!i.point.scored){H.textContent="";return}let f=i.point;H.textContent=`t=${Pe(f.timestamp)} value=${ae(f.value)} band=[${ae(f.lower)}, ${ae(f.upper)}]`+(f.isAnomaly?` \u26A0 ${f.direction} (sev ${f.severity.toFixed(2)})`:"")}}),ee=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function T(n){let e=0;for(let t of n)e+=t;return e}function ie(n){let e=n.map((t,r)=>r);return e.sort((t,r)=>n[t]<n[r]?-1:n[t]>n[r]?1:t-r),e}function se(n,e,t){let r=e.length;if(r===0)return NaN;if(n<=e[0])return t[0];if(n>=e[r-1])return t[r-1];for(let i=1;i<r;i++)if(n<=e[i]){let o=e[i-1],c=e[i],u=t[i-1],s=t[i];if(c===o)return u;let d=(n-o)/(c-o);return u+d*(s-u)}return t[r-1]}function F(n,e,t){let r=ie(n),i=r.map(b=>n[b]),o=r.map(b=>e[b]),c=T(o),u=new Array(o.length),s=0;for(let b=0;b<o.length;b++)s+=o[b],u[b]=(s-.5*o[b])/c;let d=t/100;return se(d,u,i)}function S(n,e){return F(n,e,50)}function ce(n,e,t){let r=t===void 0?S(n,e):t,i=n.map(o=>Math.abs(o-r));return S(i,e)}function ue(n,e){let t=T(e),r=0;for(let i=0;i<n.length;i++)r+=n[i]*(e[i]/t);return r}function me(n,e,t,r=0){let i=T(e),o=e.map(s=>s/i),c=t===void 0?T(n.map((s,d)=>s*o[d])):t,u=0;for(let s=0;s<n.length;s++)u+=o[s]*(n[s]-c)**2;if(r===1){let s=1-T(o.map(d=>d*d));s>1e-12&&(u/=s)}return Math.sqrt(u)}function le(n,e){return e.smoothing==="none"?n.slice():e.smoothing==="ema"?ae(n,e.smoothingAlpha):fe(n,e.smoothingWindow)}function ae(n,e){let t=n.length,r=new Array(t).fill(NaN);if(t===0)return r;let i=-1;for(let o=0;o<t;o++)if(!Number.isNaN(n[o])){i=o;break}if(i===-1)return r;r[i]=n[i];for(let o=i+1;o<t;o++)Number.isNaN(n[o])?r[o]=r[o-1]:r[o]=e*n[o]+(1-e)*r[o-1];return r}function fe(n,e){let t=n.length,r=new Array(t).fill(NaN);for(let i=0;i<t;i++){let o=Math.max(0,i-e+1),c=0,u=0;for(let s=o;s<=i;s++)Number.isNaN(n[s])||(c+=n[s],u+=1);r[i]=u>0?c/u:NaN}return r}function de(n,e){let t=n.length;if(e.inputType==="values")return n.slice();let r=new Array(t).fill(NaN);for(let i=1;i<t;i++){let o=n[i-1],c=n[i];e.inputType==="changes"?r[i]=(c-o)/o:e.inputType==="absolute_changes"?r[i]=c-o:r[i]=Math.log(c+1)-Math.log(o+1)}return r}function be(n,e){return n.halfLife!==null?n.halfLife:Math.max(n.windowSize/20,e/2,1)}function pe(n,e){if(n.windowWeights==="none")return null;let t=n.windowSize,r=new Array(t);if(n.windowWeights==="exponential"){let i=be(n,e);for(let o=1;o<=t;o++){let c=Math.min(o/i,1e3);r[o-1]=Math.pow(.5,c)}return r}for(let i=1;i<=t;i++)r[i-1]=(t+1-i)/t;return r}function K(n,e){return e===null?n.map(()=>1):n.map(t=>e[t-1])}function ge(n){let e=n[0];for(let t of n)t>e&&(e=t);return e}function ye(n){let e=n[0];for(let t of n)t<e&&(e=t);return e}function he(n,e,t){if(n.length<4)return 0;let r=(ge(e)+ye(e))/2,i=e.map(l=>l>r),o=i.map(l=>!l),c=0,u=0;for(let l=0;l<e.length;l++)i[l]?c++:u++;if(c<2||u<2)return 0;let s=(l,N)=>N.filter((L,y)=>l[y]),d=s(o,n),b=s(i,n),k=s(o,e),C=s(i,e),g=s(o,t),f=s(i,t),q=S(d,g),p=S(b,f),w=S(k,g),M=S(C,f);return M===w?0:(q-p)/(M-w)}var Se={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function Q(n,e,t){if(n==="mad"){let o=S(e,t);return{median:o,mad:ce(e,t,o)}}if(n==="zscore"){let o=ue(e,t);return{mean:o,std:me(e,t,o,1)}}let r=F(e,t,25),i=F(e,t,75);return{q1:r,q3:i,iqr:i-r}}function we(n,e,t){if(n==="mad"){if(e.mad===0)return[e.median-1e-10,e.median+1e-10];let r=t*1.4826*e.mad;return[e.median-r,e.median+r]}return n==="zscore"?e.std===0?[e.mean-1e-10,e.mean+1e-10]:[e.mean-t*e.std,e.mean+t*e.std]:e.iqr===0?[e.q1-1e-10,e.q3+1e-10]:[e.q1-t*e.iqr,e.q3+t*e.iqr]}function Me(n,e,t){if(n==="mad"){let r=1.4826*e.mad;return r>0?t/r:1/0}return n==="zscore"?e.std>0?t/e.std:1/0:e.iqr>0?t/e.iqr:1/0}function Ne(n,e){return n==="mad"?e.median:n==="zscore"?e.mean:(e.q1+e.q3)/2}function U(n,e,t,r,i){return{index:n,timestamp:e,value:t,processedValue:r,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Y(n,e){let{type:t}=e,r=Math.max(e.minSamples,X[t]),i=Se[t],{timestamps:o,values:c}=n,u=o.length,s=le(c,e),d=de(s,e),b=e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0,k=n.seasonalityData,C=pe(e,r),g=[];for(let f=0;f<u;f++){let q=c[f],p=d[f],w=o[f];if(Number.isNaN(p)){g.push(U(f,w,q,p,"missing_data"));continue}let M=Math.max(0,f-e.windowSize),l=f-M,N=d.slice(M,f),L=N.map(m=>!Number.isNaN(m)),y=[];for(let m=0;m<N.length;m++)L[m]&&y.push(N[m]);if(y.length<r){g.push(U(f,w,q,p,"insufficient_data"));continue}let O=new Array(l);for(let m=0;m<l;m++)O[m]=l-m;let z=[];for(let m=0;m<l;m++)L[m]&&z.push(O[m]);let V=K(z,C),P=0;e.detrend==="linear"&&(P=he(y,z,V));let $=P!==0?y.map((m,W)=>m+P*z[W]):y,G=Q(t,$,V),I={...G};if(b&&k){let m=k[f];for(let W of e.seasonalityComponents){let J=new Array(l).fill(!0);for(let a=0;a<l;a++){let D=k[M+a],x=!0;for(let j of W)if(!D||D[j]!==(m==null?void 0:m[j])){x=!1;break}J[a]=x}let _=[],E=[];for(let a=0;a<l;a++)L[a]&&J[a]&&(_.push(N[a]),E.push(O[a]));if(_.length<e.minSamplesPerGroup)continue;let ne=K(E,C),te=P!==0?_.map((a,D)=>a+P*E[D]):_,re=Q(t,te,ne);for(let[a,D]of i){let x=G[a],oe=(D==="spread"?x>0:x!==0)?re[a]/x:1;I[a]*=oe}}}let[h,A]=we(t,I,e.threshold);h>A&&([h,A]=[A,h]);let H=p<h||p>A,R=null,B=0;if(H){let m;p<h?(R="below",m=h-p):(R="above",m=p-A),B=Me(t,I,m)}let ee=Ne(t,I);g.push({index:f,timestamp:w,value:q,processedValue:p,scored:!0,isAnomaly:H,lower:h,upper:A,center:ee,direction:R,severity:B,reason:"ok"})}return g}function Ae(n,e){let t=n.seasonalityData;if(!t||t.length===0)return 0;let r=0;for(let i of e){let o=new Set;for(let c of t)o.add(i.map(u=>{var s;return String((s=c==null?void 0:c[u])!=null?s:"")}).join("|"));r=Math.max(r,o.size)}return r}function Z(n,e){let t=n.timestamps.length,r=Math.max(e.minSamples,X[e.type]);if(e.smoothing==="sma"&&(r=Math.max(r,e.smoothingWindow-1)),e.smoothing==="ema"&&(r=Math.max(r,Math.ceil(5/e.smoothingAlpha))),e.inputType!=="values"&&(r=Math.max(r,1)),e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0){let o=Ae(n,e.seasonalityComponents);if(o>0){let c=e.minSamplesPerGroup*o;e.windowSize>=c&&(r=Math.max(r,c))}}return Math.min(r,t)}var v=null;function De(n,e,t){let r=[],i=0;for(let o=0;o<n.length;o++){if(!(n[o].scored&&n[o].isAnomaly)){i=0;continue}let u=o>0&&n[o-1].scored&&n[o-1].isAnomaly,s=o>0&&n[o].timestamp-n[o-1].timestamp===e;i=u&&s?i+1:1,i===t&&r.push(o)}return r}self.onmessage=n=>{let e=n.data;if(e.type==="series"){v=e.series;return}if(e.type==="run"&&v){let t=e.params,r=Y(v,t),i=v.intervalSeconds*1e3,o=De(r,i,t.consecutiveAnomalies),c=Z(v,t),u=0;for(let s of r)s.scored&&s.isAnomaly&&u++;self.postMessage({type:"result",id:e.id,scored:r,fires:o,eff:c,flagged:u})}};})();
2
- `],{type:"text/javascript"})));ee.postMessage({type:"series",series:o});let J=0,Z=null;ee.onmessage=i=>{let f=i.data;if(f.type!=="result"||f.id!==J||!Z)return;let q=Z,Y=f.fires.map(_=>({t:o.timestamps[_],kind:"anomaly"}));P.render({series:o,scored:f.scored,params:q,alerts:Y}),R.textContent=`${f.flagged} flagged \xB7 ${f.fires.length} alert${f.fires.length===1?"":"s"} \xB7 warm-up ${f.eff} pts`,u.textContent=Ae(q,a)},ee.onerror=()=>{R.textContent="recompute failed \u2014 see the browser console"};let oe=()=>{Z=N(),J+=1,R.textContent="computing\u2026",ee.postMessage({type:"run",id:J,params:Z})},te=0,K=()=>{te&&window.clearTimeout(te),te=window.setTimeout(oe,130)},re=ie("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"}],r.type,i=>{var f;T((f=Ce[i])!=null?f:3),K()});h.appendChild(re.row);let ne=se("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:r.threshold,fmt:i=>i.toFixed(1)},K);h.appendChild(ne.row);let m=ne.row.querySelector("input"),b=ne.row.querySelector(".dtk-ctl-val"),T=i=>{m&&(m.value=String(i)),b&&(b.textContent=i.toFixed(1))},d=Math.max(50,Math.min(2e3,Math.floor(t/2))),g=se("Window size (points)",{min:10,max:d,step:5,value:Math.min(r.windowSize,d),fmt:i=>String(i)},K);h.appendChild(g.row);let S=ie("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],r.windowWeights,i=>{O.style.display=i==="exponential"?"":"none",K()});h.appendChild(S.row);let k=se("Half-life (points)",{min:1,max:d,step:1,value:(I=r.halfLife)!=null?I:Math.max(5,Math.round(r.windowSize/20)),fmt:i=>String(i)},K),O=k.row;O.style.display=r.windowWeights==="exponential"?"":"none",h.appendChild(O);let j=ie("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],r.detrend,K);h.appendChild(j.row);let X=ie("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],r.smoothing,K);if(h.appendChild(X.row),e.seasonality_columns.length){let i=p("div","dtk-ctl");i.appendChild(p("label","dtk-ctl-label","Seasonality conditioning"));let f=p("div","dtk-seg dtk-wrap");e.seasonality_columns.forEach(_=>{let V=p("button","dtk-seg-btn",_);V.type="button",V.classList.toggle("on",s.has(_)),V.onclick=()=>{s.has(_)?s.delete(_):s.add(_),V.classList.toggle("on",s.has(_)),K()},f.appendChild(V)}),i.appendChild(f);let q=p("label","dtk-check"),Y=p("input");Y.type="checkbox",Y.checked=C,Y.onchange=()=>{C=Y.checked,K()},q.appendChild(Y),q.appendChild(p("span",void 0,"conjoin selected into one group")),i.appendChild(q),h.appendChild(i)}let B=se("Alert: consecutive anomalies",{min:1,max:10,step:1,value:a,fmt:i=>String(i)},i=>{a=i,K()});h.appendChild(B.row);let R=p("div","dtk-tune-stat");E.appendChild(R);let W=p("div","dtk-tune-cfg");W.appendChild(p("span","dtk-tune-cfg-k","// effective config"));let u=p("code","dtk-tune-cfg-v");if(W.appendChild(u),E.appendChild(W),e.save_url){let i=p("div","dtk-tune-apply"),f=p("button","dtk-apply-btn","Apply to metric");f.type="button";let q=p("span","dtk-apply-msg");f.onclick=()=>{let Y=N();f.disabled=!0,q.className="dtk-apply-msg info",q.textContent="Applying\u2026",fetch(e.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:Y.type,params:ye(Y)},consecutive_anomalies:a})}).then(_=>_.ok?_.json():_.text().then(V=>{throw new Error(V||`HTTP ${_.status}`)})).then(_=>{var V;q.className="dtk-apply-msg ok",q.textContent=`Applied \u2192 ${(V=_.saved)!=null?V:"metric"} (previous archived). You can close this tab.`}).catch(_=>{f.disabled=!1,q.className="dtk-apply-msg err",q.textContent=`Apply failed: ${_.message}`})},i.appendChild(f),i.appendChild(q),E.appendChild(i)}else E.appendChild(p("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));oe();let y=0;window.addEventListener("resize",()=>{y&&cancelAnimationFrame(y),y=requestAnimationFrame(()=>P.resize())})}function ae(e){if(!Number.isFinite(e))return"\u2014";let n=Math.abs(e);return n!==0&&(n<.01||n>=1e6)?e.toExponential(2):Number(e.toFixed(n<1?4:2)).toString()}function Pe(e){return new Date(e).toISOString().slice(0,16).replace("T"," ")}function _e(e){return e%86400===0?`${e/86400}d`:e%3600===0?`${e/3600}h`:e%60===0?`${e/60}min`:`${e}s`}var ve=!1;function Le(){if(ve)return;ve=!0;let e=`
3
- .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
4
- --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
5
- --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
6
- --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
7
- .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
8
- .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
9
- .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
10
- .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
11
- color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
12
- .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
13
- .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
14
- .dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
15
- @media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
16
- .dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
17
- border:1px solid var(--border);border-radius:12px;padding:16px;}
18
- .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
19
- .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
20
- .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
21
- .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
22
- .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
23
- .dtk-seg.dtk-wrap{flex-wrap:wrap;}
24
- .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
25
- font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
26
- .dtk-seg-btn:hover{color:var(--ink);}
27
- .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
28
- .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
29
- .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
30
- .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
31
- .dtk-tune-chart{position:relative;width:100%;height:420px;background:var(--surface);
32
- border:1px solid var(--border);border-radius:12px;overflow:hidden;}
33
- .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
34
- .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
35
- .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
36
- .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
37
- font-size:12px;overflow-x:auto;}
38
- .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
39
- .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
40
- .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
41
- .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
42
- font-size:14px;font-weight:600;cursor:pointer;}
43
- .dtk-apply-btn:hover{background:var(--c7);}
44
- .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
45
- .dtk-apply-msg{font-size:13px;}
46
- .dtk-apply-msg.ok{color:var(--green);}
47
- .dtk-apply-msg.err{color:var(--anom);}
48
- .dtk-apply-msg.info{color:var(--muted);}
49
- .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
50
- border-radius:8px;padding:10px 12px;}
51
- `,n=document.createElement("style");n.textContent=e,document.head.appendChild(n)}window.__DTK_TUNE__={render:De};})();
File without changes
File without changes
File without changes
File without changes
File without changes