detectkit 0.30.0__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.
- {detectkit-0.30.0/detectkit.egg-info → detectkit-0.31.0}/PKG-INFO +15 -5
- {detectkit-0.30.0 → detectkit-0.31.0}/README.md +11 -2
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/__init__.py +1 -1
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/branding.py +9 -2
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/html_labeler.py +69 -7
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/cli.md +4 -2
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/tune.py +14 -14
- detectkit-0.31.0/detectkit/tuning/assets/tune.js +80 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/tuning/payload.py +39 -5
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/tuning/server.py +31 -3
- {detectkit-0.30.0 → detectkit-0.31.0/detectkit.egg-info}/PKG-INFO +15 -5
- {detectkit-0.30.0 → detectkit-0.31.0}/pyproject.toml +3 -2
- detectkit-0.30.0/detectkit/tuning/assets/tune.js +0 -50
- {detectkit-0.30.0 → detectkit-0.31.0}/LICENSE +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/MANIFEST.in +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/core/models.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/requirements.txt +0 -0
- {detectkit-0.30.0 → detectkit-0.31.0}/setup.cfg +0 -0
- {detectkit-0.30.0 → 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.
|
|
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://
|
|
8
|
-
Project-URL: Documentation, https://
|
|
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
|
-
|
|
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
|
[](https://pypi.org/project/detectkit/)
|
|
72
78
|
[](https://pypi.org/project/detectkit/)
|
|
79
|
+
[](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
|
-
|
|
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
|
[](https://pypi.org/project/detectkit/)
|
|
4
9
|
[](https://pypi.org/project/detectkit/)
|
|
10
|
+
[](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.
|
|
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 = "
|
|
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 = "
|
|
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:
|
|
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:
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
ctx.
|
|
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
|
-
|
|
513
|
-
|
|
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
|
|
82
|
-
and flagged anomalies **recompute live**.
|
|
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
|
|
@@ -13,8 +13,6 @@ new config.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
|
|
18
16
|
import click
|
|
19
17
|
|
|
20
18
|
from detectkit.cli._output import echo_done, echo_error, echo_noop
|
|
@@ -61,24 +59,26 @@ def run_tune(
|
|
|
61
59
|
|
|
62
60
|
from_dt = parse_date(from_date) if from_date else None
|
|
63
61
|
to_dt = parse_date(to_date) if to_date else None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
echo_noop(name, "no datapoints — run `dtk run --select <name> --steps load` first")
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
|
-
start = ts[0].astype("datetime64[ms]").astype(datetime)
|
|
71
|
-
end = ts[-1].astype("datetime64[ms]").astype(datetime)
|
|
62
|
+
# The builder resolves the window itself (recent ~TUNE_DEFAULT_POINTS by
|
|
63
|
+
# default, or the explicit --from/--to span) and reads only that slice — no
|
|
64
|
+
# need to pull the whole history just to find the bounds.
|
|
72
65
|
payload = build_tune_payload(
|
|
73
66
|
metric_config=config,
|
|
74
67
|
internal=internal_manager,
|
|
75
|
-
start=
|
|
76
|
-
end=
|
|
68
|
+
start=from_dt,
|
|
69
|
+
end=to_dt,
|
|
77
70
|
project_name=project_name,
|
|
78
71
|
)
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
n_points = len(payload["points"])
|
|
73
|
+
if n_points == 0:
|
|
74
|
+
echo_noop(
|
|
75
|
+
name,
|
|
76
|
+
"no datapoints in range — run `dtk run --select <name> --steps load` first, "
|
|
77
|
+
"or widen --from/--to",
|
|
78
|
+
)
|
|
81
79
|
return False
|
|
80
|
+
span = "the most recent points" if not (from_date or to_date) else "the selected window"
|
|
81
|
+
click.echo(f" Tuning on {n_points} points ({span}; pass --from/--to for a different span).")
|
|
82
82
|
|
|
83
83
|
# Static, read-only preview (no localhost server, no write-back).
|
|
84
84
|
if no_serve:
|
|
@@ -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};})();
|
|
@@ -15,7 +15,28 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
from detectkit.config.metric_config import MetricConfig
|
|
17
17
|
from detectkit.database.internal_tables import InternalTablesManager
|
|
18
|
-
from detectkit.reporting.builder import _ms, _num_or_none, _parse_seasonality
|
|
18
|
+
from detectkit.reporting.builder import _ms, _num_or_none, _parse_seasonality
|
|
19
|
+
|
|
20
|
+
# How many (most recent) points to show when no explicit window is given.
|
|
21
|
+
#
|
|
22
|
+
# Tuning recomputes the detector client-side on every knob change, which is
|
|
23
|
+
# O(points x window); baking the whole history (10k-100k points) makes the page
|
|
24
|
+
# slow to load and laggy. The renderer runs detection in a Web Worker (off the UI
|
|
25
|
+
# thread), so the budget is "how many window-touches keep a debounced recompute
|
|
26
|
+
# under ~1s" rather than "what keeps the UI from freezing". We size the default
|
|
27
|
+
# point count INVERSELY to the seeded window — small windows can afford far more
|
|
28
|
+
# points than large ones — clamped to a render/payload-comfortable range. An
|
|
29
|
+
# explicit --from/--to span is honored in full (the user opted into that cost).
|
|
30
|
+
_TUNE_COMPUTE_BUDGET = 20_000_000 # ~points x window per recompute (off-thread)
|
|
31
|
+
_TUNE_MIN_POINTS = 3000
|
|
32
|
+
_TUNE_MAX_POINTS = 15000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_window_points(seed_window: int) -> int:
|
|
36
|
+
"""Smart default point count for a seeded window size (budget-bound + clamped)."""
|
|
37
|
+
w = max(int(seed_window or 0), 1)
|
|
38
|
+
return max(_TUNE_MIN_POINTS, min(_TUNE_MAX_POINTS, round(_TUNE_COMPUTE_BUDGET / w)))
|
|
39
|
+
|
|
19
40
|
|
|
20
41
|
# Per-type interval-width defaults (mirror the detector class defaults and the
|
|
21
42
|
# website demo's DETECTOR_THRESHOLD_DEFAULT).
|
|
@@ -94,16 +115,29 @@ def build_tune_payload(
|
|
|
94
115
|
"""Assemble the interactive tuning payload from the persisted ``_dtk_datapoints``.
|
|
95
116
|
|
|
96
117
|
``save_url`` is the localhost POST endpoint the **Apply** button targets; it
|
|
97
|
-
is ``None`` for a static (read-only, no write-back) preview.
|
|
98
|
-
|
|
118
|
+
is ``None`` for a static (read-only, no write-back) preview. With no explicit
|
|
119
|
+
``start``/``end`` the window defaults to a budget-sized recent slice
|
|
120
|
+
(``default_window_points``), not the whole history.
|
|
99
121
|
"""
|
|
100
122
|
name = metric_config.name
|
|
101
123
|
interval = metric_config.get_interval()
|
|
102
124
|
interval_seconds = interval.seconds
|
|
103
125
|
|
|
104
|
-
start, end = resolve_window(internal, name, interval_seconds, start, end)
|
|
105
|
-
|
|
106
126
|
seed = _seed_detector(metric_config)
|
|
127
|
+
|
|
128
|
+
# Resolve the window. ``end`` defaults to the last datapoint. ``start``
|
|
129
|
+
# defaults to a budget-sized number of intervals before ``end`` (clamped to the
|
|
130
|
+
# first datapoint) — sized inversely to the seeded window, NOT the whole
|
|
131
|
+
# history — so the page stays interactive on large metrics. An explicit
|
|
132
|
+
# ``start`` (``--from``) is honored as-is.
|
|
133
|
+
if end is None:
|
|
134
|
+
end = internal.get_last_datapoint_timestamp(name)
|
|
135
|
+
if start is None and end is not None:
|
|
136
|
+
first = internal.get_first_datapoint_timestamp(name)
|
|
137
|
+
default_points = default_window_points(int(seed["windowSize"]))
|
|
138
|
+
lookback = end - timedelta(seconds=interval_seconds * default_points)
|
|
139
|
+
start = max(first, lookback) if first is not None else lookback
|
|
140
|
+
|
|
107
141
|
consecutive = 3
|
|
108
142
|
if metric_config.alerting:
|
|
109
143
|
consecutive = metric_config.alerting[0].consecutive_anomalies
|
|
@@ -11,11 +11,13 @@ knobs and retry.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import contextlib
|
|
14
15
|
import json
|
|
16
|
+
import os
|
|
15
17
|
import secrets
|
|
16
18
|
import threading
|
|
17
19
|
import webbrowser
|
|
18
|
-
from collections.abc import Callable
|
|
20
|
+
from collections.abc import Callable, Iterator
|
|
19
21
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from typing import Any, cast
|
|
@@ -27,6 +29,29 @@ from detectkit.tuning.html import render_tune_html
|
|
|
27
29
|
_MAX_BODY = 5_000_000 # generous cap on the posted config payload
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
@contextlib.contextmanager
|
|
33
|
+
def _quiet_stderr() -> Iterator[None]:
|
|
34
|
+
"""Silence OS-level stderr for the duration of the block.
|
|
35
|
+
|
|
36
|
+
``webbrowser.open`` shells out to ``xdg-open``, which prints a wall of
|
|
37
|
+
"browser not found" lines to stderr on a headless / WSL box. The launch is
|
|
38
|
+
best-effort (we already print the URL), so swallow that noise.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
42
|
+
except OSError:
|
|
43
|
+
yield
|
|
44
|
+
return
|
|
45
|
+
saved = os.dup(2)
|
|
46
|
+
try:
|
|
47
|
+
os.dup2(devnull, 2)
|
|
48
|
+
yield
|
|
49
|
+
finally:
|
|
50
|
+
os.dup2(saved, 2)
|
|
51
|
+
os.close(saved)
|
|
52
|
+
os.close(devnull)
|
|
53
|
+
|
|
54
|
+
|
|
30
55
|
class _TuneServer(ThreadingHTTPServer):
|
|
31
56
|
"""Localhost server holding the per-run state the handler reads/writes."""
|
|
32
57
|
|
|
@@ -136,10 +161,13 @@ def serve_tuner(
|
|
|
136
161
|
if on_ready is not None:
|
|
137
162
|
on_ready(url)
|
|
138
163
|
echo(f" Tuner: {url}")
|
|
139
|
-
echo(
|
|
164
|
+
echo(
|
|
165
|
+
" Open the URL above if no browser opens. Turn the knobs, then click Apply (Ctrl-C to cancel)."
|
|
166
|
+
)
|
|
140
167
|
if open_browser:
|
|
141
168
|
try:
|
|
142
|
-
|
|
169
|
+
with _quiet_stderr():
|
|
170
|
+
webbrowser.open(url)
|
|
143
171
|
except Exception:
|
|
144
172
|
pass
|
|
145
173
|
try:
|