detectkit 0.32.0__tar.gz → 0.33.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.32.0/detectkit.egg-info → detectkit-0.33.0}/PKG-INFO +1 -1
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/__init__.py +1 -1
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/grid_search.py +16 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/window_select.py +59 -1
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/detectors.md +9 -1
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/_windowed.py +60 -0
- detectkit-0.33.0/detectkit/tuning/assets/tune.js +82 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/tuning/payload.py +15 -2
- {detectkit-0.32.0 → detectkit-0.33.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.32.0/detectkit/tuning/assets/tune.js +0 -80
- {detectkit-0.32.0 → detectkit-0.33.0}/LICENSE +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/MANIFEST.in +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/README.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/html_labeler.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/core/models.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/pyproject.toml +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/requirements.txt +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/setup.cfg +0 -0
- {detectkit-0.32.0 → detectkit-0.33.0}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.33.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -21,6 +21,7 @@ from detectkit.autotune.window_select import (
|
|
|
21
21
|
detect_level_shift,
|
|
22
22
|
half_life_grid,
|
|
23
23
|
min_samples_for,
|
|
24
|
+
seasonal_fill_window,
|
|
24
25
|
select_window,
|
|
25
26
|
trend_present,
|
|
26
27
|
)
|
|
@@ -45,6 +46,21 @@ def grid_search(
|
|
|
45
46
|
base: dict[str, Any] = {}
|
|
46
47
|
if seasonality:
|
|
47
48
|
base["seasonality_components"] = seasonality
|
|
49
|
+
# Per-group stats engage only when the window holds min_samples_per_group
|
|
50
|
+
# points of the current seasonal key (~min_samples_per_group * cardinality
|
|
51
|
+
# points). If even the largest fold-feasible window can't reach that, the
|
|
52
|
+
# chosen seasonality will silently fall back to global at runtime — flag it
|
|
53
|
+
# in the decision log so the tuned config isn't trusted to be seasonal.
|
|
54
|
+
fill = seasonal_fill_window(tuner)
|
|
55
|
+
if fill and grid and max(grid) < fill:
|
|
56
|
+
tuner.log(
|
|
57
|
+
"window",
|
|
58
|
+
f"seasonality {seasonality} needs window_size >= {fill} to engage per-group "
|
|
59
|
+
f"stats, but the fold budget caps the window at {max(grid)} on this history "
|
|
60
|
+
"— the band will use global statistics (seasonality has no effect). Tune on "
|
|
61
|
+
"more history, or use `dtk tune` to set a larger window manually.",
|
|
62
|
+
seasonal_fill_window=fill,
|
|
63
|
+
)
|
|
48
64
|
|
|
49
65
|
has_trend = trend_present(tuner)
|
|
50
66
|
if not has_trend:
|
|
@@ -16,6 +16,15 @@ import numpy as np
|
|
|
16
16
|
from detectkit.autotune._base import _AutoTuneBase
|
|
17
17
|
from detectkit.autotune._types import CandidateEval
|
|
18
18
|
from detectkit.detectors.factory import DetectorFactory
|
|
19
|
+
from detectkit.detectors.seasonality import parse_seasonality_data
|
|
20
|
+
from detectkit.detectors.statistical._windowed import WindowedStatDetector
|
|
21
|
+
|
|
22
|
+
# Reference per-group floor used to size a seasonality-fill window candidate. The
|
|
23
|
+
# windowed detectors share this default (MAD 10 is the largest); using it makes
|
|
24
|
+
# the fill window cover every windowed type so cross-validation can actually
|
|
25
|
+
# discover whether conditioning on a seasonal key helps. See
|
|
26
|
+
# ``seasonal_fill_window``.
|
|
27
|
+
_MSPG_REF = int(WindowedStatDetector.MIN_SAMPLES_PER_GROUP_DEFAULT)
|
|
19
28
|
|
|
20
29
|
|
|
21
30
|
def min_samples_for(window_size: int, floor: int) -> int:
|
|
@@ -23,8 +32,54 @@ def min_samples_for(window_size: int, floor: int) -> int:
|
|
|
23
32
|
return min(window_size, max(floor, round(window_size / 4)))
|
|
24
33
|
|
|
25
34
|
|
|
35
|
+
def max_seasonal_cardinality(tuner: _AutoTuneBase) -> int:
|
|
36
|
+
"""Largest distinct-key count among the available single seasonality columns.
|
|
37
|
+
|
|
38
|
+
Per-group statistics only engage once the window holds
|
|
39
|
+
``min_samples_per_group`` points sharing the current point's key, and same-key
|
|
40
|
+
points recur every *cardinality* grid positions — so this is the recurrence
|
|
41
|
+
period the window must cover. We use the most granular single column (e.g.
|
|
42
|
+
``hour_of_day`` → 24) as the representative key; conjunctive groupings are
|
|
43
|
+
rarer and are backstopped by the detector's runtime under-fill warning.
|
|
44
|
+
Returns 0 when no seasonality columns are present.
|
|
45
|
+
"""
|
|
46
|
+
columns = [c for c in tuner.data.get("seasonality_columns", []) if c != "is_holiday"]
|
|
47
|
+
if not columns:
|
|
48
|
+
return 0
|
|
49
|
+
season = parse_seasonality_data(tuner.data.get("seasonality_data", np.array([])), columns)
|
|
50
|
+
card = 0
|
|
51
|
+
for col in columns:
|
|
52
|
+
vals = season.get(col)
|
|
53
|
+
if vals is None or len(vals) == 0:
|
|
54
|
+
continue
|
|
55
|
+
distinct = {v for v in vals.tolist() if v is not None}
|
|
56
|
+
card = max(card, len(distinct))
|
|
57
|
+
return card
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def seasonal_fill_window(tuner: _AutoTuneBase) -> int:
|
|
61
|
+
"""Smallest window that can fill a single-column seasonal group, else 0.
|
|
62
|
+
|
|
63
|
+
``min_samples_per_group * cardinality``. None of the natural-unit candidates
|
|
64
|
+
(≈1 day, ≈1 week) reach this for hourly ``hour_of_day`` data (24 keys → 240),
|
|
65
|
+
so without this the chosen seasonality silently never engages at the tuned
|
|
66
|
+
window. Returns 0 when there is no seasonality to fill.
|
|
67
|
+
"""
|
|
68
|
+
card = max_seasonal_cardinality(tuner)
|
|
69
|
+
return _MSPG_REF * card if card > 0 else 0
|
|
70
|
+
|
|
71
|
+
|
|
26
72
|
def window_grid(tuner: _AutoTuneBase) -> list[int]:
|
|
27
|
-
"""Candidate window sizes (≈1 day, ≈1 week, default 100), clamped to fit folds.
|
|
73
|
+
"""Candidate window sizes (≈1 day, ≈1 week, default 100), clamped to fit folds.
|
|
74
|
+
|
|
75
|
+
When the data carries seasonality columns, also offer a window large enough to
|
|
76
|
+
fill the most granular seasonal group (``min_samples_per_group * cardinality``)
|
|
77
|
+
so that — if seasonality is chosen downstream — cross-validation can evaluate a
|
|
78
|
+
window where the per-group band actually engages instead of silently falling
|
|
79
|
+
back to global statistics. Capped (like the other candidates) so it never
|
|
80
|
+
exceeds the fold budget; if it doesn't fit, seasonality simply can't fill on
|
|
81
|
+
this much history and the detector's runtime warning will say so.
|
|
82
|
+
"""
|
|
28
83
|
fixed = tuner.settings.fixed_params.get("window_size")
|
|
29
84
|
if isinstance(fixed, int):
|
|
30
85
|
return [fixed]
|
|
@@ -34,6 +89,9 @@ def window_grid(tuner: _AutoTuneBase) -> list[int]:
|
|
|
34
89
|
cap = max(20, n // (fold_count + 1))
|
|
35
90
|
pts_per_day = max(1, round(86400 / tuner.interval_seconds))
|
|
36
91
|
candidates = {100, pts_per_day, 7 * pts_per_day}
|
|
92
|
+
fill = seasonal_fill_window(tuner)
|
|
93
|
+
if fill:
|
|
94
|
+
candidates.add(fill)
|
|
37
95
|
grid = sorted({w for w in candidates if 20 <= w <= cap})
|
|
38
96
|
if not grid:
|
|
39
97
|
grid = [max(2, min(cap, 100))]
|
|
@@ -101,7 +101,15 @@ seasonality_components:
|
|
|
101
101
|
- `["hour", "day_of_week"]` — two *separate* adjustments.
|
|
102
102
|
- `[["hour", "day_of_week"]]` — one *combined* group per pair.
|
|
103
103
|
|
|
104
|
-
`min_samples_per_group` is the floor of points required per bucket.
|
|
104
|
+
`min_samples_per_group` is the floor of points required per bucket. **Size the
|
|
105
|
+
window to fill a bucket:** a group engages only when the window holds
|
|
106
|
+
`min_samples_per_group` points of the current key, and same-key points recur once
|
|
107
|
+
per *cardinality*, so `window_size ≳ min_samples_per_group × distinct_keys`. With
|
|
108
|
+
hourly `["hour"]` (24 keys, mad default 10) that's `≳ 240` — the default
|
|
109
|
+
`window_size = 100` fills no bucket, so seasonality silently falls back to the
|
|
110
|
+
global band (the detector logs a one-time warning). Raise `window_size`, lower
|
|
111
|
+
`min_samples_per_group`, or use a coarser grouping. `dtk autotune` now offers a
|
|
112
|
+
fill-sized window candidate automatically.
|
|
105
113
|
|
|
106
114
|
## Preprocessing — `input_type`
|
|
107
115
|
|
|
@@ -26,6 +26,7 @@ Subclasses define:
|
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
|
+
import logging
|
|
29
30
|
import math
|
|
30
31
|
from abc import abstractmethod
|
|
31
32
|
from typing import Any
|
|
@@ -40,6 +41,8 @@ from detectkit.detectors.seasonality import (
|
|
|
40
41
|
)
|
|
41
42
|
from detectkit.utils.stats import effective_sample_size, weighted_median
|
|
42
43
|
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
43
46
|
_INPUT_TYPES = {"values", "changes", "absolute_changes", "log_changes"}
|
|
44
47
|
_CHANGE_INPUT_TYPES = {"changes", "absolute_changes", "log_changes"}
|
|
45
48
|
_SMOOTHING_METHODS = {None, "ema", "sma"}
|
|
@@ -141,6 +144,10 @@ class WindowedStatDetector(BaseDetector):
|
|
|
141
144
|
weight_decay=weight_decay,
|
|
142
145
|
detrend=detrend,
|
|
143
146
|
)
|
|
147
|
+
# Whether the one-time "seasonality groups can't fill this window" check
|
|
148
|
+
# has run for this instance (so the warning fires at most once per run,
|
|
149
|
+
# not once per detect() batch).
|
|
150
|
+
self._underfill_checked: bool = False
|
|
144
151
|
|
|
145
152
|
# ------------------------------------------------------------------
|
|
146
153
|
# Subclass hooks
|
|
@@ -386,6 +393,56 @@ class WindowedStatDetector(BaseDetector):
|
|
|
386
393
|
# (age 0) is values + slope * age
|
|
387
394
|
return (med_new - med_old) / (age_old - age_new)
|
|
388
395
|
|
|
396
|
+
def _warn_if_groups_cannot_fill(
|
|
397
|
+
self,
|
|
398
|
+
seasonality_components: list[str | list[str]],
|
|
399
|
+
seasonality_dict: dict[str, np.ndarray],
|
|
400
|
+
window_size: int,
|
|
401
|
+
min_samples_per_group: int,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Warn once if the window is too small to ever fill a seasonality group.
|
|
404
|
+
|
|
405
|
+
Per-group statistics only engage when the trailing window holds at least
|
|
406
|
+
``min_samples_per_group`` points sharing the current point's seasonal key.
|
|
407
|
+
Same-key points recur every *cardinality* positions on the grid, so the
|
|
408
|
+
window must hold roughly ``min_samples_per_group * cardinality`` points
|
|
409
|
+
before any group fills — otherwise EVERY point silently falls back to the
|
|
410
|
+
global (un-conditioned) band and the configured seasonality has no effect.
|
|
411
|
+
This is easy to hit by accident (e.g. the default ``window_size=100`` with
|
|
412
|
+
24-key hourly seasonality needs ``>= 240``), so surface it loudly once.
|
|
413
|
+
"""
|
|
414
|
+
if self._underfill_checked:
|
|
415
|
+
return
|
|
416
|
+
self._underfill_checked = True
|
|
417
|
+
|
|
418
|
+
for group in seasonality_components:
|
|
419
|
+
cols = [group] if isinstance(group, str) else list(group)
|
|
420
|
+
present = [c for c in cols if c in seasonality_dict]
|
|
421
|
+
if not present:
|
|
422
|
+
continue
|
|
423
|
+
# Distinct seasonal keys actually present in the data (conjunction of
|
|
424
|
+
# the group's columns) — the per-key recurrence period on the grid.
|
|
425
|
+
keys = set(zip(*[seasonality_dict[c].tolist() for c in present], strict=False))
|
|
426
|
+
cardinality = len(keys)
|
|
427
|
+
if cardinality <= 0:
|
|
428
|
+
continue
|
|
429
|
+
needed = min_samples_per_group * cardinality
|
|
430
|
+
if window_size < needed:
|
|
431
|
+
logger.warning(
|
|
432
|
+
"%s: seasonality group %s has %d distinct key(s) but window_size=%d "
|
|
433
|
+
"holds only ~%d same-key point(s) (< min_samples_per_group=%d), so this "
|
|
434
|
+
"group falls back to global statistics and the seasonality has no effect. "
|
|
435
|
+
"Increase window_size to >= %d (min_samples_per_group * distinct keys), "
|
|
436
|
+
"lower min_samples_per_group, or use a coarser grouping.",
|
|
437
|
+
self.__class__.__name__,
|
|
438
|
+
cols,
|
|
439
|
+
cardinality,
|
|
440
|
+
window_size,
|
|
441
|
+
window_size // cardinality,
|
|
442
|
+
min_samples_per_group,
|
|
443
|
+
needed,
|
|
444
|
+
)
|
|
445
|
+
|
|
389
446
|
# ------------------------------------------------------------------
|
|
390
447
|
# Detection pipeline
|
|
391
448
|
# ------------------------------------------------------------------
|
|
@@ -415,6 +472,9 @@ class WindowedStatDetector(BaseDetector):
|
|
|
415
472
|
seasonality_dict = {}
|
|
416
473
|
if seasonality_components and len(seasonality_data) > 0 and seasonality_columns:
|
|
417
474
|
seasonality_dict = parse_seasonality_data(seasonality_data, seasonality_columns)
|
|
475
|
+
self._warn_if_groups_cannot_fill(
|
|
476
|
+
seasonality_components, seasonality_dict, window_size, min_samples_per_group
|
|
477
|
+
)
|
|
418
478
|
|
|
419
479
|
weight_lut = self._build_weight_lut(timestamps)
|
|
420
480
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";(()=>{var bt={"--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 X(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||bt[t]||"#888"}function gt(t){let n=t.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(t,n){let[l,e,m]=gt(t);return`rgba(${l},${e},${m},${n})`}function ot(t){let n=Math.max(1,window.devicePixelRatio||1),l=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(l*n),t.height=Math.round(e*n),n}var Ke=Number.isFinite;function Xe(t,n,l,e,m,S,o,r,u,M,h,w){let z=n.length,H=Math.max(1,Math.round(o)),A=m-e||1,W=0;for(let I=0;I<z;I++){let Y=l[I];!Ke(Y)||n[I]<e||n[I]>m||W++}if(t.strokeStyle=M,t.lineWidth=h*w,t.lineJoin="round",t.beginPath(),W<=H){let I=!1;for(let Y=0;Y<z;Y++){let re=l[Y],L=n[Y];if(!Ke(re)||L<e||L>m){I=!1;continue}let E=r(L),j=u(re);I?t.lineTo(E,j):(t.moveTo(E,j),I=!0)}}else{let I=new Array(H).fill(null),Y=new Array(H).fill(null);for(let L=0;L<z;L++){let E=l[L],j=n[L];if(!Ke(E)||j<e||j>m)continue;let U=Math.floor((j-e)/A*(H-1));U=U<0?0:U>H-1?H-1:U,(I[U]===null||E<I[U])&&(I[U]=E),(Y[U]===null||E>Y[U])&&(Y[U]=E)}let re=!1;for(let L=0;L<H;L++){if(Y[L]===null){re=!1;continue}let E=S+L,j=u(Y[L]),U=u(I[L]);re?t.lineTo(E,j):(t.moveTo(E,j),re=!0),t.lineTo(E,U)}}t.stroke()}function rt(t){let n=Math.abs(t);return n>=1e3?t.toFixed(0):n>=10?t.toFixed(1):n>=1?t.toFixed(2):t.toFixed(3)}function it(t,n){let l=new Date(t).toISOString();return n<2*864e5?l.slice(5,16).replace("T"," "):l.slice(5,10)}function st(t,n,l){return{left:n.l*l,top:n.t*l,right:t.width-n.r*l,bottom:t.height-n.b*l}}function at(t,n,l,e,m,S,o){let r=st(n,l,e),u=Math.max(r.left,Math.min(m(S),r.right));u<=r.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(r.left,r.top,u-r.left,r.bottom-r.top),t.strokeStyle=F(X("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(u,r.top),t.lineTo(u,r.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=F(X("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(o,u+6*e,r.top+5*e),t.restore())}function lt(t,n,l,e,m,S,o){let r=st(n,l,e),u=5*e;t.save();for(let M=0;M<S.length;M++){let h=S[M],w=m(h.t);if(w<r.left-1||w>r.right+1)continue;let z=o(h.kind);t.strokeStyle=F(z,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(w,r.top),t.lineTo(w,r.bottom),t.stroke(),t.fillStyle=z,t.beginPath(),t.moveTo(w-u,r.top),t.lineTo(w+u,r.top),t.lineTo(w,r.top+u*1.4),t.closePath(),t.fill()}t.restore()}var vt={mad:1,zscore:2,iqr:4};function xt(t,n){let l=t.seasonalityData;if(!l||l.length===0)return 0;let e=0;for(let m of n){let S=new Set;for(let o of l)S.add(m.map(r=>{var u;return String((u=o==null?void 0:o[r])!=null?u:"")}).join("|"));e=Math.max(e,S.size)}return e}function ut(t,n){let l=t.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,l);let e=Math.max(n.minSamples,vt[n.type]);if(n.smoothing==="sma"&&(e=Math.max(e,n.smoothingWindow-1)),n.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(e=Math.max(e,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let S=xt(t,n.seasonalityComponents);if(S>0){let o=n.minSamplesPerGroup*S;n.windowSize>=o&&(e=Math.max(e,o))}}return Math.min(e,l)}var T={l:52,r:14,t:14,b:26},Je=46,ct=10,wt=13,ae=Number.isFinite,Ce=1e3,Te=60*Ce,Ae=60*Te,Z=24*Ae,Qe=[Ce,2*Ce,5*Ce,10*Ce,15*Ce,30*Ce,Te,2*Te,5*Te,10*Te,15*Te,30*Te,Ae,2*Ae,3*Ae,6*Ae,12*Ae,Z,2*Z,3*Z,5*Z,7*Z,14*Z];function dt(t,n,l){let e=Math.max(n-t,1);if(e>l*14*Z){let r=[1,2,3,6,12,24,36,60,120,240],u=r[r.length-1];for(let A of r)if(e/(A*30.44*Z)<=l){u=A;break}let M=new Date(t),h=M.getUTCFullYear(),w=M.getUTCMonth();if(u>=12){let A=u/12;h=Math.floor(h/A)*A,w=0}else w=Math.floor(w/u)*u;let z=[],H=Date.UTC(h,w,1);for(;H<=n;)H>=t&&z.push(H),w+=u,h+=Math.floor(w/12),w%=12,H=Date.UTC(h,w,1);return{ticks:z,step:u*30*Z}}let m=Qe[Qe.length-1];for(let r of Qe)if(e/r<=l){m=r;break}let S;if(m%Z===0){let r=new Date(t);S=Date.UTC(r.getUTCFullYear(),r.getUTCMonth(),r.getUTCDate());let u=m/Z,M=Math.round(S/Z);S+=(u-M%u)%u*Z}else S=Math.ceil(t/m)*m;let o=[];for(let r=S;r<=n;r+=m)r>=t&&o.push(r);return{ticks:o,step:m}}function Ze(t,n){let l=new Date(t).toISOString();return n>=320*Z?l.slice(0,4):n>=26*Z?l.slice(0,7):n>=Z?l.slice(5,10):n>=Ae?l.slice(5,16).replace("T"," "):l.slice(5,19).replace("T"," ")}function mt(t,n={}){let l=t.getContext("2d");if(!l)throw new Error("chart: 2D context unavailable");let e=l,m=!!n.navigable,S=n.yFit==="data",o=1,r=null,u=-1,M=0,h=0,w=1,z=0,H=1,A=0,W=1,I="";function Y(){o=ot(t)}let re=()=>m?(ct+Je)*o:0,L=()=>t.width-(T.l+T.r)*o,E=()=>t.height-(T.t+T.b)*o-re(),j=()=>m?A:h,U=()=>m?W:w,le=()=>U()-j()||1,de=()=>w-h||1,O=i=>T.l*o+(i-j())/le()*L(),ie=i=>t.height-T.b*o-re()-(i-z)/(H-z||1)*E(),he=()=>t.height-Je*o,ee=()=>(Je-wt)*o,V=()=>t.width-(T.l+T.r)*o,B=i=>T.l*o+(i-h)/de()*V(),Re=i=>he()+ee()-(i-z)/(H-z||1)*ee(),De=(i,a,d)=>{let c=new Array(i.length).fill(!1);if(i.length===0)return c;let f=e.measureText(Ze(i[0],d)).width+16*o,v=-1/0;for(let y=0;y<i.length;y++){let _=a(i[y]);_-v>=f&&(c[y]=!0,v=_)}return c},me=(i,a,d)=>Math.max(a,Math.min(d,i)),te=()=>{let i=r==null?void 0:r.series,a=i?i.timestamps.length:0,d=a>1?de()/(a-1):1e3;return Math.max(d*8,1e3)};function se(i,a){let d=a-i,c=te();if(d<c){let f=(i+a)/2;i=f-c/2,a=f+c/2,d=c}d>=de()&&(i=h,a=w),i<h&&(a+=h-i,i=h),a>w&&(i-=a-w,a=w),A=me(i,h,w),W=me(a,h,w),fe()}let Be=i=>{let a=(i-T.l*o)/(L()||1);return j()+me(a,0,1)*le()},ue=i=>{let a=(i-T.l*o)/(V()||1);return h+me(a,0,1)*de()};function Oe(i){let a=r==null?void 0:r.series;if(!a||a.timestamps.length===0)return-1;let d=(i-T.l*o)/(L()||1),c=j()+d*le(),f=a.timestamps,v=0,y=f.length-1;for(;v<y;){let _=v+y>>1;f[_]<c?v=_+1:y=_}return v>0&&c-f[v-1]<f[v]-c&&(v-=1),v}function be(i,a){let d=i.timestamps;if(h=d[0],w=d[d.length-1],m){let y=`${d.length}:${h}:${w}`;y!==I?(I=y,A=h,W=w):(A=me(A,h,w),W=me(Math.max(W,A+te()),h,w))}let c=1/0,f=-1/0;for(let y of i.values)ae(y)&&(y<c&&(c=y),y>f&&(f=y));if(!S)for(let y of a)y.scored&&(ae(y.lower)&&y.lower<c&&(c=y.lower),ae(y.upper)&&y.upper>f&&(f=y.upper));(!ae(c)||!ae(f))&&(c=0,f=1),f<=c&&(f=c+1);let v=(f-c)*.06;z=c-v,H=f+v}function we(i,a,d){let c=r.series.timestamps;Xe(e,c,i,j(),U(),T.l*o,L(),O,ie,a,d,o)}function ye(i,a){let d=[],c=-1;for(let f=Math.max(0,a);f<i.length;f++){let v=i[f];v.scored&&ae(v.lower)&&ae(v.upper)?c===-1&&(c=f):c!==-1&&(d.push([c,f-1]),c=-1)}return c!==-1&&d.push([c,i.length-1]),d}function Ue(){if(M=0,!r||t.width===0||t.height===0)return;let{series:i,scored:a,params:d,alerts:c}=r;if(i.timestamps.length===0){e.fillStyle=X("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let f=X("--clay"),v=X("--st-anomaly"),y=X("--faint"),_=X("--muted");e.fillStyle=X("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*o}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let g=0;g<=4;g++){let k=z+(H-z)*g/4,x=ie(k);e.strokeStyle=F(y,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(T.l*o,x),e.lineTo(t.width-T.r*o,x),e.stroke(),e.fillStyle=_,e.textAlign="right",e.fillText(rt(k),(T.l-8)*o,x)}e.textBaseline="top";let Q=t.height-T.b*o-re();if(m){let g=dt(j(),U(),7),k=De(g.ticks,O,g.step);for(let x=0;x<g.ticks.length;x++){let D=g.ticks[x],ne=O(D);e.strokeStyle=F(y,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(ne,T.t*o),e.lineTo(ne,Q),e.stroke(),k[x]&&(e.fillStyle=_,e.textAlign=ne<(T.l+24)*o?"left":ne>t.width-(T.r+24)*o?"right":"center",e.fillText(Ze(D,g.step),ne,Q+7*o))}}else{let g=le();for(let k=0;k<=5;k++){let x=h+g*k/5,D=O(x);e.fillStyle=_,e.textAlign=k===0?"left":k===5?"right":"center",e.fillText(it(x,g),D,Q+7*o)}}e.save(),e.beginPath(),e.rect(T.l*o,T.t*o,L(),E()),e.clip();let K=a.length,R=Math.min(ut(i,d),K),$=R<K?i.timestamps[R]:void 0,q=ye(a,R);e.fillStyle=F(f,.13);for(let[g,k]of q){e.beginPath(),e.moveTo(O(a[g].timestamp),ie(a[g].upper));for(let x=g+1;x<=k;x++)e.lineTo(O(a[x].timestamp),ie(a[x].upper));for(let x=k;x>=g;x--)e.lineTo(O(a[x].timestamp),ie(a[x].lower));e.closePath(),e.fill()}e.strokeStyle=F(f,.4),e.lineWidth=1*o;for(let[g,k]of q)for(let x of["upper","lower"]){e.beginPath();for(let D=g;D<=k;D++){let ne=O(a[D].timestamp),xe=ie(a[D][x]);D===g?e.moveTo(ne,xe):e.lineTo(ne,xe)}e.stroke()}e.strokeStyle=F(y,.55),e.lineWidth=1*o,e.setLineDash([3*o,3*o]);for(let[g,k]of q){e.beginPath();for(let x=g;x<=k;x++){let D=a[x].center;if(!ae(D))continue;let ne=O(a[x].timestamp),xe=ie(D);x===g?e.moveTo(ne,xe):e.lineTo(ne,xe)}e.stroke()}if(e.setLineDash([]),d.smoothing!=="none"){we(i.values,F(f,.28),1.25);let g=a.map(k=>k.processedValue);we(g,f,1.6)}else we(i.values,f,1.5);for(let g=R;g<K;g++){let k=a[g];if(!k.scored||!ae(k.value))continue;let x=O(k.timestamp),D=ie(k.value);k.isAnomaly?(e.fillStyle=F(v,.18),e.beginPath(),e.arc(x,D,6*o,0,Math.PI*2),e.fill(),e.fillStyle=v,e.beginPath(),e.arc(x,D,3*o,0,Math.PI*2),e.fill()):i.truthAnomaly[g]&&(e.strokeStyle=F(_,.7),e.lineWidth=1.25*o,e.beginPath(),e.arc(x,D,3.5*o,0,Math.PI*2),e.stroke())}$!==void 0&&at(e,t,T,o,O,$,"detection at full power \u2192"),u>=0&&u<a.length&&ke(u,d.windowSize,a,i,y),c&&c.length&<(e,t,T,o,O,c,g=>g==="anomaly"?X("--st-anomaly"):g==="recovery"?X("--st-recovery"):X("--st-nodata")),e.restore(),m&&qe(i,c,f,y,_)}function qe(i,a,d,c,f){let v=he(),y=v+ee(),_=T.l*o,Q=t.width-T.r*o,K=dt(h,w,5);e.save(),e.beginPath(),e.rect(_,v,V(),ee()),e.clip(),e.strokeStyle=F(c,.1),e.lineWidth=1*o;for(let k of K.ticks){let x=B(k);e.beginPath(),e.moveTo(x,v),e.lineTo(x,y),e.stroke()}Xe(e,i.timestamps,i.values,h,w,_,V(),B,Re,F(d,.7),1.1,o),e.restore();let R=B(A),$=B(W);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(_,v,R-_,ee()),e.fillRect($,v,Q-$,ee()),a&&a.length){let k=X("--st-anomaly");e.fillStyle=F(k,.85);for(let x of a){let D=B(x.t);e.fillRect(D-1*o,v,2*o,ee())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(R,v,$-R,ee()),e.strokeStyle=d,e.lineWidth=1.5*o,e.strokeRect(R,v+1,$-R,ee()-2),e.fillStyle=d;let q=v+ee()/2-8*o;e.fillRect(R-2*o,q,4*o,16*o),e.fillRect($-2*o,q,4*o,16*o),e.font=`${10*o}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=f;let g=De(K.ticks,B,K.step);for(let k=0;k<K.ticks.length;k++){let x=K.ticks[k],D=B(x);e.strokeStyle=F(c,.25),e.lineWidth=1*o,e.beginPath(),e.moveTo(D,y),e.lineTo(D,y+3*o),e.stroke(),g[k]&&(e.textAlign=D<(T.l+26)*o?"left":D>t.width-(T.r+26)*o?"right":"center",e.fillText(Ze(x,K.step),D,y+5*o))}}function ke(i,a,d,c,f){let v=c.timestamps,y=Math.max(0,i-a),_=i-1,Q=T.t*o,K=E();if(_>=y){let q=c.intervalSeconds*1e3/le()*L()*.5,g=O(v[y])-q,k=O(v[_])+q;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(g,Q,k-g,K),e.strokeStyle=F(f,.5),e.lineWidth=1*o,e.beginPath(),e.moveTo(g,Q),e.lineTo(g,Q+K),e.moveTo(k,Q),e.lineTo(k,Q+K),e.stroke()}let R=O(v[i]);e.strokeStyle=F(f,.85),e.lineWidth=1*o,e.setLineDash([2*o,2*o]),e.beginPath(),e.moveTo(R,Q),e.lineTo(R,Q+K),e.stroke(),e.setLineDash([]);let $=d[i];if($.scored&&ae($.lower)&&ae($.upper)){let q=5*o;e.strokeStyle=F(X("--clay"),.85),e.lineWidth=1.5*o;for(let g of[$.lower,$.upper]){let k=ie(g);e.beginPath(),e.moveTo(R-q,k),e.lineTo(R+q,k),e.stroke()}}if(ae($.value)){let q=ie($.value);e.fillStyle=X("--term-bg"),e.beginPath(),e.arc(R,q,4*o,0,Math.PI*2),e.fill(),e.strokeStyle=$.isAnomaly?X("--st-anomaly"):X("--clay"),e.lineWidth=2*o,e.beginPath(),e.arc(R,q,4*o,0,Math.PI*2),e.stroke()}}function fe(){M===0&&(M=requestAnimationFrame(Ue))}function ge(){var d;if(!n.onHover||!r)return;if(u<0){n.onHover(null);return}let i=(d=r.scored[u])!=null?d:null,a={index:u,point:i,windowStart:Math.max(0,u-r.params.windowSize),windowEnd:u-1};n.onHover(a)}let oe=null,G=null,Se=i=>{let a=t.getBoundingClientRect();return{x:(i.clientX-a.left)*o,y:(i.clientY-a.top)*o}},Ve=i=>m&&i>=he(),Pe=i=>{let a=B(A),d=B(W),c=8*o;return Math.abs(i-a)<=c?"l":Math.abs(i-d)<=c?"r":i>a&&i<d?"move":"out"};function Le(i){if(!r)return;let{x:a,y:d}=Se(i);if(oe||G)return;if(Ve(d)){u!==-1&&(u=-1,ge(),fe());let f=Pe(a);t.style.cursor=f==="l"||f==="r"?"ew-resize":f==="move"?"grab":"pointer";return}m&&(t.style.cursor="grab");let c=Oe(a);c!==u&&(u=c,ge(),fe())}function J(){u!==-1&&(u=-1,ge(),fe())}function Me(i){if(!m||!r)return;let{x:a,y:d}=Se(i);if(d>=he()){let c=Pe(a);if(c==="l")G={type:"l",grab:0,vMin:A,vMax:W};else if(c==="r")G={type:"r",grab:0,vMin:A,vMax:W};else if(c==="move")G={type:"move",grab:ue(a),vMin:A,vMax:W};else{let f=ue(a),v=le();se(f-v/2,f+v/2),G={type:"move",grab:f,vMin:A,vMax:W}}t.style.cursor="grabbing",i.preventDefault()}else d>T.t*o&&d<he()-ct*o&&(oe={x:a,vMin:A,vMax:W},t.style.cursor="grabbing",i.preventDefault())}function _e(i){if(!r)return;let{x:a}=Se(i);if(oe){let d=(a-oe.x)*(oe.vMax-oe.vMin)/(L()||1);se(oe.vMin-d,oe.vMax-d)}else if(G){let d=ue(a);G.type==="l"?se(Math.min(d,W-te()),W):G.type==="r"?se(A,Math.max(d,A+te())):se(G.vMin+(d-G.grab),G.vMax+(d-G.grab))}}function Ee(){(oe||G)&&(oe=null,G=null,t.style.cursor="grab")}function ve(i){if(!m||!r)return;i.preventDefault();let{x:a,y:d}=Se(i),c=me(le()*Math.pow(1.0015,i.deltaY),te(),de());if(d>=he()){let f=ue(a);se(f-c/2,f+c/2)}else{let f=Be(a),v=(f-A)/(le()||1);se(f-v*c,f-v*c+c)}}function $e(){m&&se(h,w)}t.addEventListener("mousemove",Le),t.addEventListener("mouseleave",J),m&&(t.addEventListener("mousedown",Me),t.addEventListener("wheel",ve,{passive:!1}),t.addEventListener("dblclick",$e),window.addEventListener("mousemove",_e),window.addEventListener("mouseup",Ee));function Fe(i){r=i,be(i.series,i.scored),u>=i.series.timestamps.length&&(u=-1),fe()}function Ye(){Y(),r&&(be(r.series,r.scored),fe())}function Ge(){t.removeEventListener("mousemove",Le),t.removeEventListener("mouseleave",J),m&&(t.removeEventListener("mousedown",Me),t.removeEventListener("wheel",ve),t.removeEventListener("dblclick",$e),window.removeEventListener("mousemove",_e),window.removeEventListener("mouseup",Ee)),M!==0&&cancelAnimationFrame(M),M=0,r=null}return Y(),{render:Fe,resize:Ye,destroy:Ge}}var yt={mad:3,zscore:3,iqr:1.5},kt={mad:10,zscore:3,iqr:4},St="dtk-tune";function b(t,n,l){let e=document.createElement(t);return n&&(e.className=n),l!=null&&(e.textContent=l),e}function je(t,n){let l=b("label","dtk-ctl-label",t);if(n){l.title=n;let e=b("span","dtk-ctl-info","\u24D8");e.title=n,l.appendChild(document.createTextNode(" ")),l.appendChild(e)}return l}function He(t,n,l,e,m){let S=b("div","dtk-ctl");S.appendChild(je(t,m));let o=b("div","dtk-seg"),r=l,u=[],M=()=>{u.forEach(h=>h.classList.toggle("on",h.dataset.v===r))};return n.forEach(h=>{let w=b("button","dtk-seg-btn",h.label);w.type="button",w.dataset.v=h.value,w.onclick=()=>{r=h.value,M(),e(r)},u.push(w),o.appendChild(w)}),M(),S.appendChild(o),{row:S,get:()=>r,set:h=>{r=h,M()}}}function We(t,n,l){var M;let e=b("div","dtk-ctl"),m=b("div","dtk-ctl-head"),S=je(t,n.hint),o=b("span","dtk-ctl-val"),r=(M=n.fmt)!=null?M:(h=>String(h));m.appendChild(S),m.appendChild(o),e.appendChild(m);let u=b("input","dtk-range");return u.type="range",u.min=String(n.min),u.max=String(n.max),u.step=String(n.step),u.value=String(n.value),o.textContent=r(n.value),u.oninput=()=>{let h=Number(u.value);o.textContent=r(h),l(h)},e.appendChild(u),{row:e,get:()=>Number(u.value),setMax:h=>{u.max=String(h),Number(u.value)>h&&(u.value=String(h),o.textContent=r(h))}}}function pt(t){if(t.type==="manual_bounds"){let l={};return t.lowerBound!=null&&(l.lower_bound=t.lowerBound),t.upperBound!=null&&(l.upper_bound=t.upperBound),t.inputType!=="values"&&(l.input_type=t.inputType),l}let n={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(n.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(n.half_life=t.halfLife)),t.detrend!=="none"&&(n.detrend=t.detrend),t.smoothing!=="none"&&(n.smoothing=t.smoothing),t.inputType!=="values"&&(n.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(n.seasonality_components=t.seasonalityComponents,n.min_samples_per_group=t.minSamplesPerGroup),n}function Mt(t,n){let l=pt(t),e=[`type: ${t.type}`];for(let[m,S]of Object.entries(l))e.push(`${m}=${typeof S=="object"?JSON.stringify(S):S}`);return t.direction&&t.direction!=="any"&&e.push(`direction=${t.direction}`),e.push(`consecutive_anomalies=${n}`),e.join(" \xB7 ")}function Ct(t,n){var D,ne,xe;Dt(),n.classList.add(St),n.innerHTML="";let l=b("div","dtk-tune-root");n.appendChild(l);let e=t.points.length,m={timestamps:t.points.map(s=>s.t),values:t.points.map(s=>s.v==null?NaN:s.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},S=m,o=s=>{let p=Math.max(0,e-s);return p<=0?m:{timestamps:m.timestamps.slice(p),values:m.values.slice(p),intervalSeconds:m.intervalSeconds,truthAnomaly:m.truthAnomaly.slice(p),seasonalityData:m.seasonalityData?m.seasonalityData.slice(p):void 0,seasonalityColumns:m.seasonalityColumns}},r=t.detector,u=t.consecutive_anomalies,M=m.values.filter(s=>Number.isFinite(s)).sort((s,p)=>s-p),h=M.length?M[0]:0,w=M.length?M[M.length-1]:1,z=s=>M.length?M[Math.min(M.length-1,Math.max(0,Math.round(s*(M.length-1))))]:0,H=Math.max((w-h)*.05,1e-9),A=h-H,W=w+H,I=Math.max((W-A)/400,1e-9),Y=r.lowerBound!=null?r.lowerBound:z(.05),re=r.upperBound!=null?r.upperBound:z(.95),L=(D=r.seasonalityComponents)!=null?D:[],E=new Map;L.forEach((s,p)=>s.forEach(C=>E.set(C,p+1)));let j=()=>{let s=0;E.forEach(C=>{C>s&&(s=C)});let p=[];for(let C=1;C<=s;C++){let N=t.seasonality_columns.filter(P=>E.get(P)===C);N.length&&p.push(N)}return p.length?p:null},U=s=>{if(!s||!t.seasonality.length)return 0;let p=0;for(let C of s){let N=new Set;for(let P of t.seasonality)N.add(C.map(pe=>{var ze;return String((ze=P==null?void 0:P[pe])!=null?ze:"")}).join("|"));p=Math.max(p,N.size)}return p},le=()=>{var s;return{type:Me.get(),threshold:ve.get(),windowSize:a.get(),minSamples:r.minSamples,inputType:r.inputType,smoothing:y.get(),smoothingAlpha:r.smoothingAlpha,smoothingWindow:r.smoothingWindow,windowWeights:d.get(),halfLife:d.get()==="exponential"?c.get():null,detrend:v.get(),seasonalityComponents:j(),minSamplesPerGroup:(s=kt[Me.get()])!=null?s:r.minSamplesPerGroup,consecutiveAnomalies:u,direction:Q.get(),lowerBound:_e.get(),upperBound:Ee.get()}},de=b("div","dtk-tune-header"),O=b("div","dtk-tune-titlerow");O.appendChild(b("h1","dtk-tune-title",t.metric));let ie=b("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");O.appendChild(ie),de.appendChild(O);let he=t.project?`${t.project} \xB7 `:"";de.appendChild(b("div","dtk-tune-sub",`${he}${e} points \xB7 ${At(t.interval_seconds)} grid`)),t.description&&de.appendChild(b("div","dtk-tune-desc",t.description)),l.appendChild(de);let ee=b("div","dtk-tune-grid"),V=b("div","dtk-tune-controls"),B=b("div","dtk-tune-main");ee.appendChild(V),ee.appendChild(B),l.appendChild(ee);let Re=b("div","dtk-tune-trim"),De=b("div","dtk-tune-trim-head");De.appendChild(je("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 me=b("span","dtk-tune-trim-val");De.appendChild(me),Re.appendChild(De);let te=b("input","dtk-range");te.type="range",te.min=String(Math.min(e,200)),te.max=String(e),te.step=String(Math.max(1,Math.round(e/200))),te.value=String(e),Re.appendChild(te),B.appendChild(Re);let se=b("div","dtk-tune-chart"),Be=b("canvas");se.appendChild(Be);let ue=b("div","dtk-tune-spin");ue.appendChild(b("span","dtk-spin-ring")),ue.appendChild(b("span","dtk-spin-txt","computing\u2026")),se.appendChild(ue),B.appendChild(se);let Oe=b("div","dtk-tune-legend"),be=(s,p,C)=>{let N=b("span","dtk-leg-item");N.title=C,N.appendChild(b("span",`dtk-leg-sw ${s}`)),N.appendChild(b("span","dtk-leg-txt",p)),Oe.appendChild(N)};be("line","metric","The metric value over time."),be("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),be("center","band center","The expected value at the middle of the band."),be("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),be("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),B.appendChild(Oe);let we=b("div","dtk-tune-readout");B.appendChild(we);let ye=b("div","dtk-tune-warn");ye.style.display="none",B.appendChild(ye);let Ue=s=>{let p=s.seasonalityComponents,C=U(p),N=s.minSamplesPerGroup*C;p&&C>0&&s.windowSize<N?(ye.textContent=`\u26A0 Seasonality inactive at this window: ${s.windowSize} < ${N} (min_samples_per_group ${s.minSamplesPerGroup} \xD7 ${C} key${C===1?"":"s"}). Each point keeps only ~${Math.floor(s.windowSize/C)} same-key point(s), so the band falls back to global statistics (seasonality has no effect). Raise the window to \u2265 ${N}.`,ye.style.display=""):ye.style.display="none"},qe=mt(Be,{navigable:!0,yFit:"data",onHover:s=>{if(!s||!s.point||!s.point.scored){we.textContent="";return}let p=s.point;we.textContent=`t=${Tt(p.timestamp)} value=${Ie(p.value)} band=[${Ie(p.lower)}, ${Ie(p.upper)}]`+(p.isAnomaly?` \u26A0 ${p.direction} (sev ${p.severity.toFixed(2)})`:"")}}),ke=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function j(e,n,t){let o=un(e),i=o.map(b=>e[b]),r=o.map(b=>n[b]),s=C(r),c=new Array(r.length),u=0;for(let b=0;b<r.length;b++)u+=r[b],c[b]=(u-.5*r[b])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return j(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(a=>a>o),r=i.map(a=>!a),s=0,c=0;for(let a=0;a<n.length;a++)i[a]?s++:c++;if(s<2||c<2)return 0;let u=(a,M)=>M.filter((x,w)=>a[w]),d=u(r,e),b=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),m=u(i,t),A=P(d,h),p=P(b,m),S=P(N,h),g=P(y,m);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=j(n,t,25),i=j(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function F(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,b,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(b=n.upperBound)!=null?b:null,u=[];for(let y=0;y<i;y++){let h=o[y],m=r[y],A=t[y];if(Number.isNaN(m)){u.push(F(y,A,h,m,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&m<s&&(p=!0,S="below",g=s-m),c!==null&&m>c&&(p=!0,S="above",g=m-c);let a=0;if(p)if(s!==null&&c!==null){let v=c-s;a=v>0?g/v:1/0}else a=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:m,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:a,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),b=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let m=0;m<c;m++){let A=s[m],p=d[m],S=r[m];if(Number.isNaN(p)){h.push(F(m,S,A,p,"missing_data"));continue}let g=Math.max(0,m-n.windowSize),a=m-g,M=d.slice(g,m),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(F(m,S,A,p,"insufficient_data"));continue}let v=new Array(a);for(let l=0;l<a;l++)v[l]=a-l;let L=[];for(let l=0;l<a;l++)x[l]&&L.push(v[l]);let B=Q(L,y),W=0;n.detrend==="linear"&&(W=Sn(w,L,B));let nn=W!==0?w.map((l,R)=>l+W*L[R]):w,G=U(t,nn,B),_={...G};if(b&&N){let l=N[m];for(let R of n.seasonalityComponents){let K=new Array(a).fill(!0);for(let f=0;f<a;f++){let q=N[g+f],T=!0;for(let V of R)if(!q||q[V]!==(l==null?void 0:l[V])){T=!1;break}K[f]=T}let z=[],E=[];for(let f=0;f<a;f++)x[f]&&K[f]&&(z.push(M[f]),E.push(v[f]));if(z.length<n.minSamplesPerGroup)continue;let tn=Q(E,y),on=W!==0?z.map((f,q)=>f+W*E[q]):z,rn=U(t,on,tn);for(let[f,q]of i){let T=G[f],sn=(q==="spread"?T>0:T!==0)?rn[f]/T:1;_[f]*=sn}}}let[D,k]=Nn(t,_,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,O=null,J=0;if(H){let l;p<D?(O="below",l=D-p):(O="above",l=p-k),J=An(t,_,l)}let en=Mn(t,_);h.push({index:m,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:O,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var I=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function vn(e,n,t){let o=[],i=0;for(let r=0;r<e.length;r++){if(!(e[r].scored&&e[r].isAnomaly)){i=0;continue}let c=r>0&&e[r-1].scored&&e[r-1].isAnomaly,u=r>0&&e[r].timestamp-e[r-1].timestamp===n;i=c&&u?i+1:1,i===t&&o.push(r)}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){I=n.series;return}if(n.type==="run"&&I){let t=n.params,o=Z(I,t);xn(o,t.direction);let i=I.intervalSeconds*1e3,r=vn(o,i,t.consecutiveAnomalies),s=$(I,t),c=0;for(let u of o)u.scored&&u.isAnomaly&&c++;self.postMessage({type:"result",id:n.id,scored:o,fires:r,eff:s,flagged:c})}};})();
|
|
2
|
+
`],{type:"text/javascript"})));ke.postMessage({type:"series",series:S});let fe=0,ge=null;ke.onmessage=s=>{let p=s.data;if(p.type!=="result"||p.id!==fe||!ge)return;ue.classList.remove("on");let C=ge,N=p.fires.map(P=>({t:S.timestamps[P],kind:"anomaly"}));qe.render({series:S,scored:p.scored,params:C,alerts:N}),q.textContent=`${p.flagged} flagged \xB7 ${p.fires.length} alert${p.fires.length===1?"":"s"} \xB7 warm-up ${p.eff} pts`,Ue(C),k.textContent=Mt(C,u)},ke.onerror=()=>{ue.classList.remove("on"),q.textContent="recompute failed \u2014 see the browser console"};let oe=()=>{ge=le(),fe+=1,ue.classList.add("on"),ke.postMessage({type:"run",id:fe,params:ge})},G=s=>s>1?et(m.timestamps[e-1]-m.timestamps[e-s]):"\u2014",Se=s=>{me.textContent=s>=e?`${e} pts \xB7 full (${G(e)})`:`${s} pts \xB7 ${G(s)}`};Se(e);function Ve(s){S=o(s),ke.postMessage({type:"series",series:S}),J()}let Pe=0;te.oninput=()=>{let s=Number(te.value);Se(s),Pe&&window.clearTimeout(Pe),Pe=window.setTimeout(()=>Ve(s),200)};let Le=0,J=()=>{Le&&window.clearTimeout(Le),Le=window.setTimeout(oe,130)},Me=He("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],r.type,s=>{var p;Ye((p=yt[s])!=null?p:3),$(),J()},"The statistic for the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles) \u2014 all windowed \u2014 or Manual (fixed lower/upper thresholds, no window/history).");V.appendChild(Me.row);let _e=We("Lower bound",{min:A,max:W,step:I,value:Y,fmt:s=>Ie(s),hint:"Manual bounds: values below this read as anomalous. Drag in from the data range to see how many points fall outside (and how many alerts that yields)."},J);V.appendChild(_e.row);let Ee=We("Upper bound",{min:A,max:W,step:I,value:re,fmt:s=>Ie(s),hint:"Manual bounds: values above this read as anomalous."},J);V.appendChild(Ee.row);let ve=We("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:r.threshold,fmt:s=>s.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},J);V.appendChild(ve.row);let $e=ve.row.querySelector("input"),Fe=ve.row.querySelector(".dtk-ctl-val"),Ye=s=>{$e&&($e.value=String(s)),Fe&&(Fe.textContent=s.toFixed(1))},Ge=Math.max(50,Math.min(2e3,Math.floor(e/2))),i=Math.max(Ge,r.windowSize),a=We("Window size (points)",{min:Math.max(1,Math.min(10,r.windowSize)),max:i,step:1,value:r.windowSize,fmt:s=>`${s} \xB7 ${et(s*t.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},J);V.appendChild(a.row);let d=He("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],r.windowWeights,()=>{$(),J()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");V.appendChild(d.row);let c=We("Half-life (points)",{min:1,max:i,step:1,value:(ne=r.halfLife)!=null?ne:Math.max(5,Math.round(r.windowSize/20)),fmt:s=>`${s} \xB7 ${et(s*t.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},J),f=c.row;f.style.display=r.windowWeights==="exponential"?"":"none",V.appendChild(f);let v=He("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],r.detrend,J,"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.");V.appendChild(v.row);let y=He("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],r.smoothing,J,"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.");V.appendChild(y.row);let _=null;if(t.seasonality_columns.length){let s=t.seasonality_columns,p=b("div","dtk-ctl");_=p,p.appendChild(je("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 C=Math.min(s.length,6),N=[{label:"\u2014",value:"0"}];for(let P=1;P<=C;P++)N.push({label:`G${P}`,value:String(P)});s.forEach(P=>{let pe=b("div","dtk-season-row");pe.appendChild(b("span","dtk-season-col",P));let ze=b("div","dtk-seg dtk-season-seg"),tt=[],ht=()=>{var ce;return(ce=E.get(P))!=null?ce:0},nt=()=>tt.forEach(ce=>ce.classList.toggle("on",Number(ce.dataset.v)===ht()));N.forEach(ce=>{let Ne=b("button","dtk-seg-btn",ce.label);Ne.type="button",Ne.dataset.v=ce.value,Ne.title=ce.value==="0"?`ignore ${P}`:`put ${P} in group ${ce.value}`,Ne.onclick=()=>{E.set(P,Number(ce.value)),nt(),J()},tt.push(Ne),ze.appendChild(Ne)}),nt(),pe.appendChild(ze),p.appendChild(pe)}),V.appendChild(p)}let Q=He("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(xe=t.direction)!=null?xe:"any",J,"Which anomalies to show and count toward alerts: both directions, only spikes ABOVE the band (up) or only drops BELOW it (down). A preview filter mirroring the alert direction policy \u2014 it never changes the band itself.");V.appendChild(Q.row);let K=We("Alert: consecutive anomalies",{min:1,max:10,step:1,value:u,fmt:s=>String(s),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},s=>{u=s,J()});V.appendChild(K.row);let R=[ve.row,a.row,d.row,v.row,y.row];_&&R.push(_);function $(){let s=Me.get()==="manual_bounds";for(let p of R)p.style.display=s?"none":"";_e.row.style.display=s?"":"none",Ee.row.style.display=s?"":"none",f.style.display=!s&&d.get()==="exponential"?"":"none"}$();let q=b("div","dtk-tune-stat");B.appendChild(q);let g=b("div","dtk-tune-cfg");g.appendChild(b("span","dtk-tune-cfg-k","// effective config"));let k=b("code","dtk-tune-cfg-v");if(g.appendChild(k),B.appendChild(g),t.save_url){let s=b("div","dtk-tune-apply"),p=b("button","dtk-apply-btn","Apply to metric");p.type="button",p.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 C=b("span","dtk-apply-msg");p.onclick=()=>{let N=le();p.disabled=!0,C.className="dtk-apply-msg info",C.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:N.type,params:pt(N)},consecutive_anomalies:u})}).then(P=>P.ok?P.json():P.text().then(pe=>{throw new Error(pe||`HTTP ${P.status}`)})).then(P=>{var pe;C.className="dtk-apply-msg ok",C.textContent=`Applied \u2192 ${(pe=P.saved)!=null?pe:"metric"} (previous archived). You can close this tab.`}).catch(P=>{p.disabled=!1,C.className="dtk-apply-msg err",C.textContent=`Apply failed: ${P.message}`})},s.appendChild(p),s.appendChild(C),B.appendChild(s)}else B.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."));oe();let x=0;window.addEventListener("resize",()=>{x&&cancelAnimationFrame(x),x=requestAnimationFrame(()=>qe.resize())})}function Ie(t){if(!Number.isFinite(t))return"\u2014";let n=Math.abs(t);return n!==0&&(n<.01||n>=1e6)?t.toExponential(2):Number(t.toFixed(n<1?4:2)).toString()}function Tt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function et(t){let n=Math.round(t/6e4);if(n<60)return`${n}m`;let l=Math.floor(n/60);if(l<24)return`${l}h`;let e=Math.floor(l/24),m=l%24;return m?`${e}d ${m}h`:`${e}d`}function At(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var ft=!1;function Dt(){if(ft)return;ft=!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-warn{font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--c7);
|
|
37
|
+
background:rgba(240,173,78,0.13);border:1px solid rgba(240,173,78,0.5);border-radius:8px;padding:8px 11px;}
|
|
38
|
+
.dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
|
|
39
|
+
font-size:12px;overflow-x:auto;}
|
|
40
|
+
.dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
|
|
41
|
+
.dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
|
|
42
|
+
.dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
|
|
43
|
+
.dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
|
|
44
|
+
font-size:14px;font-weight:600;cursor:pointer;}
|
|
45
|
+
.dtk-apply-btn:hover{background:var(--c7);}
|
|
46
|
+
.dtk-apply-btn:disabled{opacity:.55;cursor:default;}
|
|
47
|
+
.dtk-apply-msg{font-size:13px;}
|
|
48
|
+
.dtk-apply-msg.ok{color:var(--green);}
|
|
49
|
+
.dtk-apply-msg.err{color:var(--anom);}
|
|
50
|
+
.dtk-apply-msg.info{color:var(--muted);}
|
|
51
|
+
.dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
|
|
52
|
+
border-radius:8px;padding:10px 12px;}
|
|
53
|
+
.dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
|
|
54
|
+
.dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
|
|
55
|
+
border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
|
|
56
|
+
.dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
|
|
57
|
+
.dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
|
|
58
|
+
.dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
|
|
59
|
+
background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
|
|
60
|
+
padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
|
|
61
|
+
.dtk-tune-spin.on{display:inline-flex;}
|
|
62
|
+
.dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
|
|
63
|
+
border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
|
|
64
|
+
@keyframes dtk-spin{to{transform:rotate(360deg);}}
|
|
65
|
+
.dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
|
|
66
|
+
.dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
|
|
67
|
+
.dtk-leg-sw{display:inline-block;flex:0 0 auto;}
|
|
68
|
+
.dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
|
|
69
|
+
.dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
|
|
70
|
+
border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
|
|
71
|
+
.dtk-leg-sw.center{width:16px;height:2px;
|
|
72
|
+
background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
|
|
73
|
+
.dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
|
|
74
|
+
.dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
|
|
75
|
+
border-top:7px solid var(--anom);}
|
|
76
|
+
.dtk-leg-txt{white-space:nowrap;}
|
|
77
|
+
.dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
78
|
+
.dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
|
|
79
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
80
|
+
.dtk-season-seg{flex:0 0 auto;padding:2px;}
|
|
81
|
+
.dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
|
|
82
|
+
`,n=document.createElement("style");n.textContent=t,document.head.appendChild(n)}window.__DTK_TUNE__={render:Ct};})();
|
|
@@ -30,12 +30,25 @@ from detectkit.reporting.builder import _ms, _num_or_none, _parse_seasonality
|
|
|
30
30
|
_TUNE_COMPUTE_BUDGET = 20_000_000 # ~points x window per recompute (off-thread)
|
|
31
31
|
_TUNE_MIN_POINTS = 3000
|
|
32
32
|
_TUNE_MAX_POINTS = 15000
|
|
33
|
+
# A point only gets a band where the trailing window can fill, so showing fewer
|
|
34
|
+
# than a few windows of history would leave a large-window metric with almost no
|
|
35
|
+
# scored region (the band never reaches its real width). Floor the shown points
|
|
36
|
+
# at this many windows so the seeded window is actually exercised in the preview.
|
|
37
|
+
_TUNE_WINDOW_COVERAGE = 3
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def default_window_points(seed_window: int) -> int:
|
|
36
|
-
"""Smart default point count for a seeded window size
|
|
41
|
+
"""Smart default point count for a seeded window size.
|
|
42
|
+
|
|
43
|
+
Inversely sized to the window under a recompute budget (small windows can
|
|
44
|
+
afford far more points), but floored at a few windows' worth of history so a
|
|
45
|
+
large-window metric still has a meaningful scored region, and clamped to a
|
|
46
|
+
render/payload-comfortable range.
|
|
47
|
+
"""
|
|
37
48
|
w = max(int(seed_window or 0), 1)
|
|
38
|
-
|
|
49
|
+
budget_points = round(_TUNE_COMPUTE_BUDGET / w)
|
|
50
|
+
fill_points = w * _TUNE_WINDOW_COVERAGE
|
|
51
|
+
return max(_TUNE_MIN_POINTS, min(_TUNE_MAX_POINTS, max(budget_points, fill_points)))
|
|
39
52
|
|
|
40
53
|
|
|
41
54
|
# Per-type interval-width defaults (mirror the detector class defaults and the
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use strict";(()=>{var mt={"--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 V(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||mt[t]||"#888"}function ft(t){let n=t.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let a=parseInt(n,16);return n.length!==6||Number.isNaN(a)?[209,91,54]:[a>>16&255,a>>8&255,a&255]}function O(t,n){let[a,e,d]=ft(t);return`rgba(${a},${e},${d},${n})`}function Ze(t){let n=Math.max(1,window.devicePixelRatio||1),a=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(a*n),t.height=Math.round(e*n),n}var je=Number.isFinite;function Ue(t,n,a,e,d,o,b,s,f,g,h,M){let j=n.length,C=Math.max(1,Math.round(b)),D=d-e||1,ue=0;for(let R=0;R<j;R++){let q=a[R];!je(q)||n[R]<e||n[R]>d||ue++}if(t.strokeStyle=g,t.lineWidth=h*M,t.lineJoin="round",t.beginPath(),ue<=C){let R=!1;for(let q=0;q<j;q++){let Y=a[q],W=n[q];if(!je(Y)||W<e||W>d){R=!1;continue}let P=s(W),J=f(Y);R?t.lineTo(P,J):(t.moveTo(P,J),R=!0)}}else{let R=new Array(C).fill(null),q=new Array(C).fill(null);for(let W=0;W<j;W++){let P=a[W],J=n[W];if(!je(P)||J<e||J>d)continue;let L=Math.floor((J-e)/D*(C-1));L=L<0?0:L>C-1?C-1:L,(R[L]===null||P<R[L])&&(R[L]=P),(q[L]===null||P>q[L])&&(q[L]=P)}let Y=!1;for(let W=0;W<C;W++){if(q[W]===null){Y=!1;continue}let P=o+W,J=f(q[W]),L=f(R[W]);Y?t.lineTo(P,J):(t.moveTo(P,J),Y=!0),t.lineTo(P,L)}}t.stroke()}function et(t){let n=Math.abs(t);return n>=1e3?t.toFixed(0):n>=10?t.toFixed(1):n>=1?t.toFixed(2):t.toFixed(3)}function tt(t,n){let a=new Date(t).toISOString();return n<2*864e5?a.slice(5,16).replace("T"," "):a.slice(5,10)}function nt(t,n,a){return{left:n.l*a,top:n.t*a,right:t.width-n.r*a,bottom:t.height-n.b*a}}function ot(t,n,a,e,d,o,b){let s=nt(n,a,e),f=Math.max(s.left,Math.min(d(o),s.right));f<=s.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(s.left,s.top,f-s.left,s.bottom-s.top),t.strokeStyle=O(V("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(f,s.top),t.lineTo(f,s.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=O(V("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(b,f+6*e,s.top+5*e),t.restore())}function rt(t,n,a,e,d,o,b){let s=nt(n,a,e),f=5*e;t.save();for(let g=0;g<o.length;g++){let h=o[g],M=d(h.t);if(M<s.left-1||M>s.right+1)continue;let j=b(h.kind);t.strokeStyle=O(j,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(M,s.top),t.lineTo(M,s.bottom),t.stroke(),t.fillStyle=j,t.beginPath(),t.moveTo(M-f,s.top),t.lineTo(M+f,s.top),t.lineTo(M,s.top+f*1.4),t.closePath(),t.fill()}t.restore()}var pt={mad:1,zscore:2,iqr:4};function ht(t,n){let a=t.seasonalityData;if(!a||a.length===0)return 0;let e=0;for(let d of n){let o=new Set;for(let b of a)o.add(d.map(s=>{var f;return String((f=b==null?void 0:b[s])!=null?f:"")}).join("|"));e=Math.max(e,o.size)}return e}function it(t,n){let a=t.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,a);let e=Math.max(n.minSamples,pt[n.type]);if(n.smoothing==="sma"&&(e=Math.max(e,n.smoothingWindow-1)),n.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(e=Math.max(e,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let o=ht(t,n.seasonalityComponents);if(o>0){let b=n.minSamplesPerGroup*o;n.windowSize>=b&&(e=Math.max(e,b))}}return Math.min(e,a)}var T={l:52,r:14,t:14,b:26},Ve=46,st=10,bt=13,re=Number.isFinite,we=1e3,ye=60*we,ke=60*ye,X=24*ke,Ye=[we,2*we,5*we,10*we,15*we,30*we,ye,2*ye,5*ye,10*ye,15*ye,30*ye,ke,2*ke,3*ke,6*ke,12*ke,X,2*X,3*X,5*X,7*X,14*X];function lt(t,n,a){let e=Math.max(n-t,1);if(e>a*14*X){let s=[1,2,3,6,12,24,36,60,120,240],f=s[s.length-1];for(let D of s)if(e/(D*30.44*X)<=a){f=D;break}let g=new Date(t),h=g.getUTCFullYear(),M=g.getUTCMonth();if(f>=12){let D=f/12;h=Math.floor(h/D)*D,M=0}else M=Math.floor(M/f)*f;let j=[],C=Date.UTC(h,M,1);for(;C<=n;)C>=t&&j.push(C),M+=f,h+=Math.floor(M/12),M%=12,C=Date.UTC(h,M,1);return{ticks:j,step:f*30*X}}let d=Ye[Ye.length-1];for(let s of Ye)if(e/s<=a){d=s;break}let o;if(d%X===0){let s=new Date(t);o=Date.UTC(s.getUTCFullYear(),s.getUTCMonth(),s.getUTCDate());let f=d/X,g=Math.round(o/X);o+=(f-g%f)%f*X}else o=Math.ceil(t/d)*d;let b=[];for(let s=o;s<=n;s+=d)s>=t&&b.push(s);return{ticks:b,step:d}}function Ge(t,n){let a=new Date(t).toISOString();return n>=320*X?a.slice(0,4):n>=26*X?a.slice(0,7):n>=X?a.slice(5,10):n>=ke?a.slice(5,16).replace("T"," "):a.slice(5,19).replace("T"," ")}function at(t,n={}){let a=t.getContext("2d");if(!a)throw new Error("chart: 2D context unavailable");let e=a,d=!!n.navigable,o=1,b=null,s=-1,f=0,g=0,h=1,M=0,j=1,C=0,D=1,ue="";function R(){o=Ze(t)}let q=()=>d?(st+Ve)*o:0,Y=()=>t.width-(T.l+T.r)*o,W=()=>t.height-(T.t+T.b)*o-q(),P=()=>d?C:g,J=()=>d?D:h,L=()=>J()-P()||1,le=()=>h-g||1,H=i=>T.l*o+(i-P())/L()*Y(),ne=i=>t.height-T.b*o-q()-(i-M)/(j-M||1)*W(),de=()=>t.height-Ve*o,Z=()=>(Ve-bt)*o,F=()=>t.width-(T.l+T.r)*o,z=i=>T.l*o+(i-g)/le()*F(),Le=i=>de()+Z()-(i-M)/(j-M||1)*Z(),Se=(i,l,m)=>{let u=new Array(i.length).fill(!1);if(i.length===0)return u;let p=e.measureText(Ge(i[0],m)).width+16*o,w=-1/0;for(let S=0;S<i.length;S++){let _=l(i[S]);_-w>=p&&(u[S]=!0,w=_)}return u},ae=(i,l,m)=>Math.max(l,Math.min(m,i)),ee=()=>{let i=b==null?void 0:b.series,l=i?i.timestamps.length:0,m=l>1?le()/(l-1):1e3;return Math.max(m*8,1e3)};function oe(i,l){let m=l-i,u=ee();if(m<u){let p=(i+l)/2;i=p-u/2,l=p+u/2,m=u}m>=le()&&(i=g,l=h),i<g&&(l+=g-i,i=g),l>h&&(i-=l-h,l=h),C=ae(i,g,h),D=ae(l,g,h),ce()}let Ie=i=>{let l=(i-T.l*o)/(Y()||1);return P()+ae(l,0,1)*L()},ie=i=>{let l=(i-T.l*o)/(F()||1);return g+ae(l,0,1)*le()};function Be(i){let l=b==null?void 0:b.series;if(!l||l.timestamps.length===0)return-1;let m=(i-T.l*o)/(Y()||1),u=P()+m*L(),p=l.timestamps,w=0,S=p.length-1;for(;w<S;){let _=w+S>>1;p[_]<u?w=_+1:S=_}return w>0&&u-p[w-1]<p[w]-u&&(w-=1),w}function me(i,l){let m=i.timestamps;if(g=m[0],h=m[m.length-1],d){let S=`${m.length}:${g}:${h}`;S!==ue?(ue=S,C=g,D=h):(C=ae(C,g,h),D=ae(Math.max(D,C+ee()),g,h))}let u=1/0,p=-1/0;for(let S of i.values)re(S)&&(S<u&&(u=S),S>p&&(p=S));for(let S of l)S.scored&&(re(S.lower)&&S.lower<u&&(u=S.lower),re(S.upper)&&S.upper>p&&(p=S.upper));(!re(u)||!re(p))&&(u=0,p=1),p<=u&&(p=u+1);let w=(p-u)*.06;M=u-w,j=p+w}function be(i,l,m){let u=b.series.timestamps;Ue(e,u,i,P(),J(),T.l*o,Y(),H,ne,l,m,o)}function Oe(i,l){let m=[],u=-1;for(let p=Math.max(0,l);p<i.length;p++){let w=i[p];w.scored&&re(w.lower)&&re(w.upper)?u===-1&&(u=p):u!==-1&&(m.push([u,p-1]),u=-1)}return u!==-1&&m.push([u,i.length-1]),m}function ge(){if(f=0,!b||t.width===0||t.height===0)return;let{series:i,scored:l,params:m,alerts:u}=b;if(i.timestamps.length===0){e.fillStyle=V("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let p=V("--clay"),w=V("--st-anomaly"),S=V("--faint"),_=V("--muted");e.fillStyle=V("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*o}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let x=0;x<=4;x++){let y=M+(j-M)*x/4,k=ne(y);e.strokeStyle=O(S,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(T.l*o,k),e.lineTo(t.width-T.r*o,k),e.stroke(),e.fillStyle=_,e.textAlign="right",e.fillText(et(y),(T.l-8)*o,k)}e.textBaseline="top";let U=t.height-T.b*o-q();if(d){let x=lt(P(),J(),7),y=Se(x.ticks,H,x.step);for(let k=0;k<x.ticks.length;k++){let r=x.ticks[k],c=H(r);e.strokeStyle=O(S,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(c,T.t*o),e.lineTo(c,U),e.stroke(),y[k]&&(e.fillStyle=_,e.textAlign=c<(T.l+24)*o?"left":c>t.width-(T.r+24)*o?"right":"center",e.fillText(Ge(r,x.step),c,U+7*o))}}else{let x=L();for(let y=0;y<=5;y++){let k=g+x*y/5,r=H(k);e.fillStyle=_,e.textAlign=y===0?"left":y===5?"right":"center",e.fillText(tt(k,x),r,U+7*o)}}e.save(),e.beginPath(),e.rect(T.l*o,T.t*o,Y(),W()),e.clip();let I=l.length,E=Math.min(it(i,m),I),$=E<I?i.timestamps[E]:void 0,B=Oe(l,E);e.fillStyle=O(p,.13);for(let[x,y]of B){e.beginPath(),e.moveTo(H(l[x].timestamp),ne(l[x].upper));for(let k=x+1;k<=y;k++)e.lineTo(H(l[k].timestamp),ne(l[k].upper));for(let k=y;k>=x;k--)e.lineTo(H(l[k].timestamp),ne(l[k].lower));e.closePath(),e.fill()}e.strokeStyle=O(p,.4),e.lineWidth=1*o;for(let[x,y]of B)for(let k of["upper","lower"]){e.beginPath();for(let r=x;r<=y;r++){let c=H(l[r].timestamp),A=ne(l[r][k]);r===x?e.moveTo(c,A):e.lineTo(c,A)}e.stroke()}e.strokeStyle=O(S,.55),e.lineWidth=1*o,e.setLineDash([3*o,3*o]);for(let[x,y]of B){e.beginPath();for(let k=x;k<=y;k++){let r=l[k].center;if(!re(r))continue;let c=H(l[k].timestamp),A=ne(r);k===x?e.moveTo(c,A):e.lineTo(c,A)}e.stroke()}if(e.setLineDash([]),m.smoothing!=="none"){be(i.values,O(p,.28),1.25);let x=l.map(y=>y.processedValue);be(x,p,1.6)}else be(i.values,p,1.5);for(let x=E;x<I;x++){let y=l[x];if(!y.scored||!re(y.value))continue;let k=H(y.timestamp),r=ne(y.value);y.isAnomaly?(e.fillStyle=O(w,.18),e.beginPath(),e.arc(k,r,6*o,0,Math.PI*2),e.fill(),e.fillStyle=w,e.beginPath(),e.arc(k,r,3*o,0,Math.PI*2),e.fill()):i.truthAnomaly[x]&&(e.strokeStyle=O(_,.7),e.lineWidth=1.25*o,e.beginPath(),e.arc(k,r,3.5*o,0,Math.PI*2),e.stroke())}$!==void 0&&ot(e,t,T,o,H,$,"detection at full power \u2192"),s>=0&&s<l.length&&Me(s,m.windowSize,l,i,S),u&&u.length&&rt(e,t,T,o,H,u,x=>x==="anomaly"?V("--st-anomaly"):x==="recovery"?V("--st-recovery"):V("--st-nodata")),e.restore(),d&&_e(i,u,p,S,_)}function _e(i,l,m,u,p){let w=de(),S=w+Z(),_=T.l*o,U=t.width-T.r*o,I=lt(g,h,5);e.save(),e.beginPath(),e.rect(_,w,F(),Z()),e.clip(),e.strokeStyle=O(u,.1),e.lineWidth=1*o;for(let y of I.ticks){let k=z(y);e.beginPath(),e.moveTo(k,w),e.lineTo(k,S),e.stroke()}Ue(e,i.timestamps,i.values,g,h,_,F(),z,Le,O(m,.7),1.1,o),e.restore();let E=z(C),$=z(D);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(_,w,E-_,Z()),e.fillRect($,w,U-$,Z()),l&&l.length){let y=V("--st-anomaly");e.fillStyle=O(y,.85);for(let k of l){let r=z(k.t);e.fillRect(r-1*o,w,2*o,Z())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(E,w,$-E,Z()),e.strokeStyle=m,e.lineWidth=1.5*o,e.strokeRect(E,w+1,$-E,Z()-2),e.fillStyle=m;let B=w+Z()/2-8*o;e.fillRect(E-2*o,B,4*o,16*o),e.fillRect($-2*o,B,4*o,16*o),e.font=`${10*o}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=p;let x=Se(I.ticks,z,I.step);for(let y=0;y<I.ticks.length;y++){let k=I.ticks[y],r=z(k);e.strokeStyle=O(u,.25),e.lineWidth=1*o,e.beginPath(),e.moveTo(r,S),e.lineTo(r,S+3*o),e.stroke(),x[y]&&(e.textAlign=r<(T.l+26)*o?"left":r>t.width-(T.r+26)*o?"right":"center",e.fillText(Ge(k,I.step),r,S+5*o))}}function Me(i,l,m,u,p){let w=u.timestamps,S=Math.max(0,i-l),_=i-1,U=T.t*o,I=W();if(_>=S){let B=u.intervalSeconds*1e3/L()*Y()*.5,x=H(w[S])-B,y=H(w[_])+B;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(x,U,y-x,I),e.strokeStyle=O(p,.5),e.lineWidth=1*o,e.beginPath(),e.moveTo(x,U),e.lineTo(x,U+I),e.moveTo(y,U),e.lineTo(y,U+I),e.stroke()}let E=H(w[i]);e.strokeStyle=O(p,.85),e.lineWidth=1*o,e.setLineDash([2*o,2*o]),e.beginPath(),e.moveTo(E,U),e.lineTo(E,U+I),e.stroke(),e.setLineDash([]);let $=m[i];if($.scored&&re($.lower)&&re($.upper)){let B=5*o;e.strokeStyle=O(V("--clay"),.85),e.lineWidth=1.5*o;for(let x of[$.lower,$.upper]){let y=ne(x);e.beginPath(),e.moveTo(E-B,y),e.lineTo(E+B,y),e.stroke()}}if(re($.value)){let B=ne($.value);e.fillStyle=V("--term-bg"),e.beginPath(),e.arc(E,B,4*o,0,Math.PI*2),e.fill(),e.strokeStyle=$.isAnomaly?V("--st-anomaly"):V("--clay"),e.lineWidth=2*o,e.beginPath(),e.arc(E,B,4*o,0,Math.PI*2),e.stroke()}}function ce(){f===0&&(f=requestAnimationFrame(ge))}function Ce(){var m;if(!n.onHover||!b)return;if(s<0){n.onHover(null);return}let i=(m=b.scored[s])!=null?m:null,l={index:s,point:i,windowStart:Math.max(0,s-b.params.windowSize),windowEnd:s-1};n.onHover(l)}let te=null,G=null,fe=i=>{let l=t.getBoundingClientRect();return{x:(i.clientX-l.left)*o,y:(i.clientY-l.top)*o}},Ne=i=>d&&i>=de(),K=i=>{let l=z(C),m=z(D),u=8*o;return Math.abs(i-l)<=u?"l":Math.abs(i-m)<=u?"r":i>l&&i<m?"move":"out"};function ve(i){if(!b)return;let{x:l,y:m}=fe(i);if(te||G)return;if(Ne(m)){s!==-1&&(s=-1,Ce(),ce());let p=K(l);t.style.cursor=p==="l"||p==="r"?"ew-resize":p==="move"?"grab":"pointer";return}d&&(t.style.cursor="grab");let u=Be(l);u!==s&&(s=u,Ce(),ce())}function Te(){s!==-1&&(s=-1,Ce(),ce())}function Ae(i){if(!d||!b)return;let{x:l,y:m}=fe(i);if(m>=de()){let u=K(l);if(u==="l")G={type:"l",grab:0,vMin:C,vMax:D};else if(u==="r")G={type:"r",grab:0,vMin:C,vMax:D};else if(u==="move")G={type:"move",grab:ie(l),vMin:C,vMax:D};else{let p=ie(l),w=L();oe(p-w/2,p+w/2),G={type:"move",grab:p,vMin:C,vMax:D}}t.style.cursor="grabbing",i.preventDefault()}else m>T.t*o&&m<de()-st*o&&(te={x:l,vMin:C,vMax:D},t.style.cursor="grabbing",i.preventDefault())}function pe(i){if(!b)return;let{x:l}=fe(i);if(te){let m=(l-te.x)*(te.vMax-te.vMin)/(Y()||1);oe(te.vMin-m,te.vMax-m)}else if(G){let m=ie(l);G.type==="l"?oe(Math.min(m,D-ee()),D):G.type==="r"?oe(C,Math.max(m,C+ee())):oe(G.vMin+(m-G.grab),G.vMax+(m-G.grab))}}function Ee(){(te||G)&&(te=null,G=null,t.style.cursor="grab")}function We(i){if(!d||!b)return;i.preventDefault();let{x:l,y:m}=fe(i),u=ae(L()*Math.pow(1.0015,i.deltaY),ee(),le());if(m>=de()){let p=ie(l);oe(p-u/2,p+u/2)}else{let p=Ie(l),w=(p-C)/(L()||1);oe(p-w*u,p-w*u+u)}}function qe(){d&&oe(g,h)}t.addEventListener("mousemove",ve),t.addEventListener("mouseleave",Te),d&&(t.addEventListener("mousedown",Ae),t.addEventListener("wheel",We,{passive:!1}),t.addEventListener("dblclick",qe),window.addEventListener("mousemove",pe),window.addEventListener("mouseup",Ee));function Re(i){b=i,me(i.series,i.scored),s>=i.series.timestamps.length&&(s=-1),ce()}function $e(){R(),b&&(me(b.series,b.scored),ce())}function xe(){t.removeEventListener("mousemove",ve),t.removeEventListener("mouseleave",Te),d&&(t.removeEventListener("mousedown",Ae),t.removeEventListener("wheel",We),t.removeEventListener("dblclick",qe),window.removeEventListener("mousemove",pe),window.removeEventListener("mouseup",Ee)),f!==0&&cancelAnimationFrame(f),f=0,b=null}return R(),{render:Re,resize:$e,destroy:xe}}var gt={mad:3,zscore:3,iqr:1.5},vt={mad:10,zscore:3,iqr:4},xt="dtk-tune";function v(t,n,a){let e=document.createElement(t);return n&&(e.className=n),a!=null&&(e.textContent=a),e}function Fe(t,n){let a=v("label","dtk-ctl-label",t);if(n){a.title=n;let e=v("span","dtk-ctl-info","\u24D8");e.title=n,a.appendChild(document.createTextNode(" ")),a.appendChild(e)}return a}function He(t,n,a,e,d){let o=v("div","dtk-ctl");o.appendChild(Fe(t,d));let b=v("div","dtk-seg"),s=a,f=[],g=()=>{f.forEach(h=>h.classList.toggle("on",h.dataset.v===s))};return n.forEach(h=>{let M=v("button","dtk-seg-btn",h.label);M.type="button",M.dataset.v=h.value,M.onclick=()=>{s=h.value,g(),e(s)},f.push(M),b.appendChild(M)}),g(),o.appendChild(b),{row:o,get:()=>s,set:h=>{s=h,g()}}}function Pe(t,n,a){var g;let e=v("div","dtk-ctl"),d=v("div","dtk-ctl-head"),o=Fe(t,n.hint),b=v("span","dtk-ctl-val"),s=(g=n.fmt)!=null?g:(h=>String(h));d.appendChild(o),d.appendChild(b),e.appendChild(d);let f=v("input","dtk-range");return f.type="range",f.min=String(n.min),f.max=String(n.max),f.step=String(n.step),f.value=String(n.value),b.textContent=s(n.value),f.oninput=()=>{let h=Number(f.value);b.textContent=s(h),a(h)},e.appendChild(f),{row:e,get:()=>Number(f.value),setMax:h=>{f.max=String(h),Number(f.value)>h&&(f.value=String(h),b.textContent=s(h))}}}function ct(t){if(t.type==="manual_bounds"){let a={};return t.lowerBound!=null&&(a.lower_bound=t.lowerBound),t.upperBound!=null&&(a.upper_bound=t.upperBound),t.inputType!=="values"&&(a.input_type=t.inputType),a}let n={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(n.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(n.half_life=t.halfLife)),t.detrend!=="none"&&(n.detrend=t.detrend),t.smoothing!=="none"&&(n.smoothing=t.smoothing),t.inputType!=="values"&&(n.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(n.seasonality_components=t.seasonalityComponents,n.min_samples_per_group=t.minSamplesPerGroup),n}function wt(t,n){let a=ct(t),e=[`type: ${t.type}`];for(let[d,o]of Object.entries(a))e.push(`${d}=${typeof o=="object"?JSON.stringify(o):o}`);return t.direction&&t.direction!=="any"&&e.push(`direction=${t.direction}`),e.push(`consecutive_anomalies=${n}`),e.join(" \xB7 ")}function yt(t,n){var x,y,k;Mt(),n.classList.add(xt),n.innerHTML="";let a=v("div","dtk-tune-root");n.appendChild(a);let e=t.points.length,d={timestamps:t.points.map(r=>r.t),values:t.points.map(r=>r.v==null?NaN:r.v),intervalSeconds:t.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:t.seasonality_columns.length?t.seasonality:void 0,seasonalityColumns:t.seasonality_columns.length?t.seasonality_columns:void 0},o=d,b=r=>{let c=Math.max(0,e-r);return c<=0?d:{timestamps:d.timestamps.slice(c),values:d.values.slice(c),intervalSeconds:d.intervalSeconds,truthAnomaly:d.truthAnomaly.slice(c),seasonalityData:d.seasonalityData?d.seasonalityData.slice(c):void 0,seasonalityColumns:d.seasonalityColumns}},s=t.detector,f=t.consecutive_anomalies,g=d.values.filter(r=>Number.isFinite(r)).sort((r,c)=>r-c),h=g.length?g[0]:0,M=g.length?g[g.length-1]:1,j=r=>g.length?g[Math.min(g.length-1,Math.max(0,Math.round(r*(g.length-1))))]:0,C=Math.max((M-h)*.05,1e-9),D=h-C,ue=M+C,R=Math.max((ue-D)/400,1e-9),q=s.lowerBound!=null?s.lowerBound:j(.05),Y=s.upperBound!=null?s.upperBound:j(.95),W=(x=s.seasonalityComponents)!=null?x:[],P=new Map;W.forEach((r,c)=>r.forEach(A=>P.set(A,c+1)));let J=()=>{let r=0;P.forEach(A=>{A>r&&(r=A)});let c=[];for(let A=1;A<=r;A++){let Q=t.seasonality_columns.filter(N=>P.get(N)===A);Q.length&&c.push(Q)}return c.length?c:null},L=()=>{var r;return{type:ve.get(),threshold:pe.get(),windowSize:$e.get(),minSamples:s.minSamples,inputType:s.inputType,smoothing:u.get(),smoothingAlpha:s.smoothingAlpha,smoothingWindow:s.smoothingWindow,windowWeights:xe.get(),halfLife:xe.get()==="exponential"?i.get():null,detrend:m.get(),seasonalityComponents:J(),minSamplesPerGroup:(r=vt[ve.get()])!=null?r:s.minSamplesPerGroup,consecutiveAnomalies:f,direction:w.get(),lowerBound:Te.get(),upperBound:Ae.get()}},le=v("div","dtk-tune-header"),H=v("div","dtk-tune-titlerow");H.appendChild(v("h1","dtk-tune-title",t.metric));let ne=v("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");H.appendChild(ne),le.appendChild(H);let de=t.project?`${t.project} \xB7 `:"";le.appendChild(v("div","dtk-tune-sub",`${de}${e} points \xB7 ${St(t.interval_seconds)} grid`)),t.description&&le.appendChild(v("div","dtk-tune-desc",t.description)),a.appendChild(le);let Z=v("div","dtk-tune-grid"),F=v("div","dtk-tune-controls"),z=v("div","dtk-tune-main");Z.appendChild(F),Z.appendChild(z),a.appendChild(Z);let Le=v("div","dtk-tune-trim"),Se=v("div","dtk-tune-trim-head");Se.appendChild(Fe("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 ae=v("span","dtk-tune-trim-val");Se.appendChild(ae),Le.appendChild(Se);let ee=v("input","dtk-range");ee.type="range",ee.min=String(Math.min(e,200)),ee.max=String(e),ee.step=String(Math.max(1,Math.round(e/200))),ee.value=String(e),Le.appendChild(ee),z.appendChild(Le);let oe=v("div","dtk-tune-chart"),Ie=v("canvas");oe.appendChild(Ie);let ie=v("div","dtk-tune-spin");ie.appendChild(v("span","dtk-spin-ring")),ie.appendChild(v("span","dtk-spin-txt","computing\u2026")),oe.appendChild(ie),z.appendChild(oe);let Be=v("div","dtk-tune-legend"),me=(r,c,A)=>{let Q=v("span","dtk-leg-item");Q.title=A,Q.appendChild(v("span",`dtk-leg-sw ${r}`)),Q.appendChild(v("span","dtk-leg-txt",c)),Be.appendChild(Q)};me("line","metric","The metric value over time."),me("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),me("center","band center","The expected value at the middle of the band."),me("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),me("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),z.appendChild(Be);let be=v("div","dtk-tune-readout");z.appendChild(be);let Oe=at(Ie,{navigable:!0,onHover:r=>{if(!r||!r.point||!r.point.scored){be.textContent="";return}let c=r.point;be.textContent=`t=${kt(c.timestamp)} value=${ze(c.value)} band=[${ze(c.lower)}, ${ze(c.upper)}]`+(c.isAnomaly?` \u26A0 ${c.direction} (sev ${c.severity.toFixed(2)})`:"")}}),ge=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function j(e,n,t){let o=un(e),i=o.map(b=>e[b]),r=o.map(b=>n[b]),s=C(r),c=new Array(r.length),u=0;for(let b=0;b<r.length;b++)u+=r[b],c[b]=(u-.5*r[b])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return j(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(a=>a>o),r=i.map(a=>!a),s=0,c=0;for(let a=0;a<n.length;a++)i[a]?s++:c++;if(s<2||c<2)return 0;let u=(a,M)=>M.filter((x,w)=>a[w]),d=u(r,e),b=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),m=u(i,t),A=P(d,h),p=P(b,m),S=P(N,h),g=P(y,m);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=j(n,t,25),i=j(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function F(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,b,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(b=n.upperBound)!=null?b:null,u=[];for(let y=0;y<i;y++){let h=o[y],m=r[y],A=t[y];if(Number.isNaN(m)){u.push(F(y,A,h,m,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&m<s&&(p=!0,S="below",g=s-m),c!==null&&m>c&&(p=!0,S="above",g=m-c);let a=0;if(p)if(s!==null&&c!==null){let v=c-s;a=v>0?g/v:1/0}else a=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:m,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:a,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),b=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let m=0;m<c;m++){let A=s[m],p=d[m],S=r[m];if(Number.isNaN(p)){h.push(F(m,S,A,p,"missing_data"));continue}let g=Math.max(0,m-n.windowSize),a=m-g,M=d.slice(g,m),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(F(m,S,A,p,"insufficient_data"));continue}let v=new Array(a);for(let l=0;l<a;l++)v[l]=a-l;let L=[];for(let l=0;l<a;l++)x[l]&&L.push(v[l]);let B=Q(L,y),W=0;n.detrend==="linear"&&(W=Sn(w,L,B));let nn=W!==0?w.map((l,R)=>l+W*L[R]):w,G=U(t,nn,B),_={...G};if(b&&N){let l=N[m];for(let R of n.seasonalityComponents){let K=new Array(a).fill(!0);for(let f=0;f<a;f++){let q=N[g+f],T=!0;for(let V of R)if(!q||q[V]!==(l==null?void 0:l[V])){T=!1;break}K[f]=T}let z=[],E=[];for(let f=0;f<a;f++)x[f]&&K[f]&&(z.push(M[f]),E.push(v[f]));if(z.length<n.minSamplesPerGroup)continue;let tn=Q(E,y),on=W!==0?z.map((f,q)=>f+W*E[q]):z,rn=U(t,on,tn);for(let[f,q]of i){let T=G[f],sn=(q==="spread"?T>0:T!==0)?rn[f]/T:1;_[f]*=sn}}}let[D,k]=Nn(t,_,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,O=null,J=0;if(H){let l;p<D?(O="below",l=D-p):(O="above",l=p-k),J=An(t,_,l)}let en=Mn(t,_);h.push({index:m,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:O,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var I=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function vn(e,n,t){let o=[],i=0;for(let r=0;r<e.length;r++){if(!(e[r].scored&&e[r].isAnomaly)){i=0;continue}let c=r>0&&e[r-1].scored&&e[r-1].isAnomaly,u=r>0&&e[r].timestamp-e[r-1].timestamp===n;i=c&&u?i+1:1,i===t&&o.push(r)}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){I=n.series;return}if(n.type==="run"&&I){let t=n.params,o=Z(I,t);xn(o,t.direction);let i=I.intervalSeconds*1e3,r=vn(o,i,t.consecutiveAnomalies),s=$(I,t),c=0;for(let u of o)u.scored&&u.isAnomaly&&c++;self.postMessage({type:"result",id:n.id,scored:o,fires:r,eff:s,flagged:c})}};})();
|
|
2
|
-
`],{type:"text/javascript"})));ge.postMessage({type:"series",series:o});let _e=0,Me=null;ge.onmessage=r=>{let c=r.data;if(c.type!=="result"||c.id!==_e||!Me)return;ie.classList.remove("on");let A=Me,Q=c.fires.map(N=>({t:o.timestamps[N],kind:"anomaly"}));Oe.render({series:o,scored:c.scored,params:A,alerts:Q}),I.textContent=`${c.flagged} flagged \xB7 ${c.fires.length} alert${c.fires.length===1?"":"s"} \xB7 warm-up ${c.eff} pts`,$.textContent=wt(A,f)},ge.onerror=()=>{ie.classList.remove("on"),I.textContent="recompute failed \u2014 see the browser console"};let ce=()=>{Me=L(),_e+=1,ie.classList.add("on"),ge.postMessage({type:"run",id:_e,params:Me})},Ce=r=>r>1?Ke(d.timestamps[e-1]-d.timestamps[e-r]):"\u2014",te=r=>{ae.textContent=r>=e?`${e} pts \xB7 full (${Ce(e)})`:`${r} pts \xB7 ${Ce(r)}`};te(e);function G(r){o=b(r),ge.postMessage({type:"series",series:o}),K()}let fe=0;ee.oninput=()=>{let r=Number(ee.value);te(r),fe&&window.clearTimeout(fe),fe=window.setTimeout(()=>G(r),200)};let Ne=0,K=()=>{Ne&&window.clearTimeout(Ne),Ne=window.setTimeout(ce,130)},ve=He("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],s.type,r=>{var c;qe((c=gt[r])!=null?c:3),U(),K()},"The statistic for the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles) \u2014 all windowed \u2014 or Manual (fixed lower/upper thresholds, no window/history).");F.appendChild(ve.row);let Te=Pe("Lower bound",{min:D,max:ue,step:R,value:q,fmt:r=>ze(r),hint:"Manual bounds: values below this read as anomalous. Drag in from the data range to see how many points fall outside (and how many alerts that yields)."},K);F.appendChild(Te.row);let Ae=Pe("Upper bound",{min:D,max:ue,step:R,value:Y,fmt:r=>ze(r),hint:"Manual bounds: values above this read as anomalous."},K);F.appendChild(Ae.row);let pe=Pe("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:s.threshold,fmt:r=>r.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},K);F.appendChild(pe.row);let Ee=pe.row.querySelector("input"),We=pe.row.querySelector(".dtk-ctl-val"),qe=r=>{Ee&&(Ee.value=String(r)),We&&(We.textContent=r.toFixed(1))},Re=Math.max(50,Math.min(2e3,Math.floor(e/2))),$e=Pe("Window size (points)",{min:10,max:Re,step:5,value:Math.min(s.windowSize,Re),fmt:r=>`${r} \xB7 ${Ke(r*t.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},K);F.appendChild($e.row);let xe=He("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],s.windowWeights,()=>{U(),K()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");F.appendChild(xe.row);let i=Pe("Half-life (points)",{min:1,max:Re,step:1,value:(y=s.halfLife)!=null?y:Math.max(5,Math.round(s.windowSize/20)),fmt:r=>`${r} \xB7 ${Ke(r*t.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},K),l=i.row;l.style.display=s.windowWeights==="exponential"?"":"none",F.appendChild(l);let m=He("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],s.detrend,K,"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.");F.appendChild(m.row);let u=He("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],s.smoothing,K,"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.");F.appendChild(u.row);let p=null;if(t.seasonality_columns.length){let r=t.seasonality_columns,c=v("div","dtk-ctl");p=c,c.appendChild(Fe("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(r.length,6),Q=[{label:"\u2014",value:"0"}];for(let N=1;N<=A;N++)Q.push({label:`G${N}`,value:String(N)});r.forEach(N=>{let he=v("div","dtk-season-row");he.appendChild(v("span","dtk-season-col",N));let Xe=v("div","dtk-seg dtk-season-seg"),Je=[],dt=()=>{var se;return(se=P.get(N))!=null?se:0},Qe=()=>Je.forEach(se=>se.classList.toggle("on",Number(se.dataset.v)===dt()));Q.forEach(se=>{let De=v("button","dtk-seg-btn",se.label);De.type="button",De.dataset.v=se.value,De.title=se.value==="0"?`ignore ${N}`:`put ${N} in group ${se.value}`,De.onclick=()=>{P.set(N,Number(se.value)),Qe(),K()},Je.push(De),Xe.appendChild(De)}),Qe(),he.appendChild(Xe),c.appendChild(he)}),F.appendChild(c)}let w=He("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(k=t.direction)!=null?k:"any",K,"Which anomalies to show and count toward alerts: both directions, only spikes ABOVE the band (up) or only drops BELOW it (down). A preview filter mirroring the alert direction policy \u2014 it never changes the band itself.");F.appendChild(w.row);let S=Pe("Alert: consecutive anomalies",{min:1,max:10,step:1,value:f,fmt:r=>String(r),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},r=>{f=r,K()});F.appendChild(S.row);let _=[pe.row,$e.row,xe.row,m.row,u.row];p&&_.push(p);function U(){let r=ve.get()==="manual_bounds";for(let c of _)c.style.display=r?"none":"";Te.row.style.display=r?"":"none",Ae.row.style.display=r?"":"none",l.style.display=!r&&xe.get()==="exponential"?"":"none"}U();let I=v("div","dtk-tune-stat");z.appendChild(I);let E=v("div","dtk-tune-cfg");E.appendChild(v("span","dtk-tune-cfg-k","// effective config"));let $=v("code","dtk-tune-cfg-v");if(E.appendChild($),z.appendChild(E),t.save_url){let r=v("div","dtk-tune-apply"),c=v("button","dtk-apply-btn","Apply to metric");c.type="button",c.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=v("span","dtk-apply-msg");c.onclick=()=>{let Q=L();c.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:Q.type,params:ct(Q)},consecutive_anomalies:f})}).then(N=>N.ok?N.json():N.text().then(he=>{throw new Error(he||`HTTP ${N.status}`)})).then(N=>{var he;A.className="dtk-apply-msg ok",A.textContent=`Applied \u2192 ${(he=N.saved)!=null?he:"metric"} (previous archived). You can close this tab.`}).catch(N=>{c.disabled=!1,A.className="dtk-apply-msg err",A.textContent=`Apply failed: ${N.message}`})},r.appendChild(c),r.appendChild(A),z.appendChild(r)}else z.appendChild(v("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."));ce();let B=0;window.addEventListener("resize",()=>{B&&cancelAnimationFrame(B),B=requestAnimationFrame(()=>Oe.resize())})}function ze(t){if(!Number.isFinite(t))return"\u2014";let n=Math.abs(t);return n!==0&&(n<.01||n>=1e6)?t.toExponential(2):Number(t.toFixed(n<1?4:2)).toString()}function kt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function Ke(t){let n=Math.round(t/6e4);if(n<60)return`${n}m`;let a=Math.floor(n/60);if(a<24)return`${a}h`;let e=Math.floor(a/24),d=a%24;return d?`${e}d ${d}h`:`${e}d`}function St(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var ut=!1;function Mt(){if(ut)return;ut=!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
|
-
`,n=document.createElement("style");n.textContent=t,document.head.appendChild(n)}window.__DTK_TUNE__={render:yt};})();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.32.0 → detectkit-0.33.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|