detectkit 0.39.2__tar.gz → 0.40.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.39.2/detectkit.egg-info → detectkit-0.40.0}/PKG-INFO +1 -1
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/__init__.py +1 -1
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/cli.md +12 -6
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/metrics.md +7 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/project.md +6 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/tune.py +6 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/config/metric_config.py +23 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/config/project_config.py +24 -0
- detectkit-0.40.0/detectkit/tuning/assets/tune.js +176 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/tuning/payload.py +9 -0
- {detectkit-0.39.2 → detectkit-0.40.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.39.2/detectkit/tuning/assets/tune.js +0 -171
- {detectkit-0.39.2 → detectkit-0.40.0}/LICENSE +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/MANIFEST.in +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/README.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/html_labeler.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/core/models.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/pyproject.toml +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/requirements.txt +0 -0
- {detectkit-0.39.2 → detectkit-0.40.0}/setup.cfg +0 -0
- {detectkit-0.39.2 → detectkit-0.40.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.40.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -104,17 +104,23 @@ the lot), and **Label** (band hides; **mark the real incidents** by drag, **Lass
|
|
|
104
104
|
anomalies** — loop a cloud of anomaly dots, each consecutive run, gaps bridged up to
|
|
105
105
|
`consecutive_anomalies`, becomes one span — or **Threshold capture** every span past
|
|
106
106
|
a horizontal line, each widened to a full interval so the alert lands inside; the
|
|
107
|
-
painted window saves as `capture_windows`).
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
painted window saves as `capture_windows`). **Confirming an alert valid IS marking an
|
|
108
|
+
incident**: the confirmed streak becomes a first-class **ground-truth incident** that
|
|
109
|
+
shows in the Marked-incidents list (a read-only "✓ confirmed alert" row; its ✕
|
|
110
|
+
un-confirms the alert), counts toward recall + correct, and is written on Save — so a
|
|
111
|
+
clean metric whose alerts are all good is validated in a few clicks **without
|
|
112
|
+
hand-drawing spans**. The list, the live metrics and Save share **one** ground-truth
|
|
113
|
+
set (hand-marked spans **plus** confirmed-valid alerts, deduped by overlap).
|
|
111
114
|
As you tune, a metrics bar shows **incident catch rate (recall)** — how many
|
|
112
|
-
ground-truth incidents (marked +
|
|
115
|
+
ground-truth incidents (marked + confirmed) your config catches (an incident is
|
|
113
116
|
caught when an alert's anomaly **streak overlaps** it, not just the fire instant) —
|
|
114
117
|
**false-alert rate** (what share of alerts fall outside any incident and aren't
|
|
115
118
|
confirmed valid; "≈1 in N false", a decimal so a mostly-false rate doesn't round to
|
|
116
119
|
a misleading "1 in 1") — and **reviewed N/M**; only incidents within the loaded
|
|
117
|
-
window are scored. **
|
|
120
|
+
window are scored. An optional **false-alert budget** (`false_alert_budget`, a
|
|
121
|
+
fraction in `(0, 1]`, on the **metric** then **project**, default `0.5`) gently flags
|
|
122
|
+
the false-alert chip when the rate exceeds it (tuning-only; labeling stays optional).
|
|
123
|
+
**Save incidents** writes a
|
|
118
124
|
versioned `incidents/<metric>/*.yml` — the same store `dtk autotune` reads, so the
|
|
119
125
|
same labels feed the next supervised tune (it seeds from the newest file on open,
|
|
120
126
|
**anchoring the budget-sized loaded window on the seeded incidents** — ending just
|
|
@@ -54,8 +54,15 @@ alerting: # optional — see alerting.md
|
|
|
54
54
|
tables: # optional — per-metric internal table overrides
|
|
55
55
|
datapoints: _dtk_datapoints_api
|
|
56
56
|
detections: _dtk_detections_api
|
|
57
|
+
|
|
58
|
+
false_alert_budget: 0.3 # optional — `dtk tune` target false-alert rate (0,1]; overrides project
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
`false_alert_budget` is a per-metric **target false-alert rate** (a fraction in
|
|
62
|
+
`(0, 1]`) the `dtk tune` cockpit gently flags when exceeded. It overrides the
|
|
63
|
+
project-wide default; unset → project, then a built-in `0.5`. Tuning-only — it never
|
|
64
|
+
affects the load/detect/alert pipeline.
|
|
65
|
+
|
|
59
66
|
## `interval` (required)
|
|
60
67
|
|
|
61
68
|
Point spacing. String (`"30s"`, `"1min"`, `"5min"`, `"10min"`, `"1hour"`,
|
|
@@ -31,11 +31,17 @@ timeouts: # per-step, seconds
|
|
|
31
31
|
alert: 300 # alert step (default 300)
|
|
32
32
|
|
|
33
33
|
alert_help_url: null # optional, see below — "How to read this alert" link
|
|
34
|
+
false_alert_budget: null # optional — `dtk tune` target FDR (0,1]; per-metric override wins
|
|
34
35
|
|
|
35
36
|
error_alerting: # optional, see below
|
|
36
37
|
enabled: false
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
`false_alert_budget` is a project-wide **target false-alert rate** (a fraction in
|
|
41
|
+
`(0, 1]`, e.g. `0.3` = 30%) for manual tuning: the `dtk tune` cockpit gently flags a
|
|
42
|
+
metric when its false-alert rate exceeds it. A per-metric `false_alert_budget`
|
|
43
|
+
overrides it; unset → a built-in `0.5`. Tuning-only — it never touches the pipeline.
|
|
44
|
+
|
|
39
45
|
### `alert_help_url` — "How to read this alert" link
|
|
40
46
|
|
|
41
47
|
Every default-rendered alert on every channel carries a `How to read this alert`
|
|
@@ -93,6 +93,11 @@ def run_tune(
|
|
|
93
93
|
|
|
94
94
|
from_dt = parse_date(from_date) if from_date else None
|
|
95
95
|
to_dt = parse_date(to_date) if to_date else None
|
|
96
|
+
# False-alert-rate budget for the cockpit's quality bar: metric overrides
|
|
97
|
+
# project; the builder falls back to a built-in default when both are unset.
|
|
98
|
+
budget = config.false_alert_budget
|
|
99
|
+
if budget is None:
|
|
100
|
+
budget = getattr(project_config, "false_alert_budget", None)
|
|
96
101
|
# The builder resolves the window itself (recent ~TUNE_DEFAULT_POINTS by
|
|
97
102
|
# default, or the explicit --from/--to span) and reads only that slice — no
|
|
98
103
|
# need to pull the whole history just to find the bounds.
|
|
@@ -105,6 +110,7 @@ def run_tune(
|
|
|
105
110
|
incidents=preload_incidents,
|
|
106
111
|
capture_windows=preload_capture,
|
|
107
112
|
alert_reviews=preload_reviews,
|
|
113
|
+
false_alert_budget=budget,
|
|
108
114
|
)
|
|
109
115
|
n_points = len(payload["points"])
|
|
110
116
|
if n_points == 0:
|
|
@@ -612,8 +612,31 @@ class MetricConfig(BaseModel):
|
|
|
612
612
|
autotune: AutoTuneConfig | None = Field(
|
|
613
613
|
default=None, description="Optional auto-tuning constraints (for `dtk autotune`)"
|
|
614
614
|
)
|
|
615
|
+
# Per-metric false-alert-rate (FDR) budget for manual tuning, overriding the
|
|
616
|
+
# project-wide `false_alert_budget`. The `dtk tune` cockpit flags — non
|
|
617
|
+
# intrusively — when the share of fired alerts that don't overlap a real
|
|
618
|
+
# incident exceeds this fraction. Tuning-only: it never affects load/detect/
|
|
619
|
+
# alert and labeling stays optional.
|
|
620
|
+
false_alert_budget: float | None = Field(
|
|
621
|
+
default=None,
|
|
622
|
+
description=(
|
|
623
|
+
"False-alert-rate budget (a fraction in (0, 1], e.g. 0.3 = 30%) the "
|
|
624
|
+
"`dtk tune` cockpit flags when exceeded. Overrides the project-wide "
|
|
625
|
+
"default; unset → fall back to project, then a built-in default."
|
|
626
|
+
),
|
|
627
|
+
)
|
|
615
628
|
enabled: bool = Field(default=True, description="Whether metric is enabled")
|
|
616
629
|
|
|
630
|
+
@field_validator("false_alert_budget")
|
|
631
|
+
@classmethod
|
|
632
|
+
def validate_false_alert_budget(cls, v: float | None) -> float | None:
|
|
633
|
+
"""A budget, if set, is a false-alert-rate fraction in ``(0, 1]``."""
|
|
634
|
+
if v is None:
|
|
635
|
+
return v
|
|
636
|
+
if not 0.0 < v <= 1.0:
|
|
637
|
+
raise ValueError("false_alert_budget must be a fraction in (0, 1] (e.g. 0.3 = 30%)")
|
|
638
|
+
return v
|
|
639
|
+
|
|
617
640
|
# Parsed interval (computed from string/int)
|
|
618
641
|
_interval: Interval | None = None
|
|
619
642
|
|
|
@@ -196,6 +196,30 @@ class ProjectConfig(BaseModel):
|
|
|
196
196
|
"or false to hide it."
|
|
197
197
|
),
|
|
198
198
|
)
|
|
199
|
+
# Project-wide default false-alert-rate (FDR) budget for manual tuning. The
|
|
200
|
+
# `dtk tune` cockpit flags — non-intrusively — when the share of fired alerts
|
|
201
|
+
# that don't overlap a real incident exceeds this fraction, so you have a
|
|
202
|
+
# target to tune against. A per-metric `false_alert_budget` overrides it.
|
|
203
|
+
# Labeling stays optional: the budget only colours an already-computed number,
|
|
204
|
+
# it never blocks anything or affects the load/detect/alert pipeline.
|
|
205
|
+
false_alert_budget: float | None = Field(
|
|
206
|
+
default=None,
|
|
207
|
+
description=(
|
|
208
|
+
"Default false-alert-rate budget (a fraction in (0, 1], e.g. 0.3 = "
|
|
209
|
+
"30%) the `dtk tune` cockpit flags when exceeded. Per-metric "
|
|
210
|
+
"`false_alert_budget` takes priority; unset → a built-in default."
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@field_validator("false_alert_budget")
|
|
215
|
+
@classmethod
|
|
216
|
+
def validate_false_alert_budget(cls, v: float | None) -> float | None:
|
|
217
|
+
"""A budget, if set, is a false-alert-rate fraction in ``(0, 1]``."""
|
|
218
|
+
if v is None:
|
|
219
|
+
return v
|
|
220
|
+
if not 0.0 < v <= 1.0:
|
|
221
|
+
raise ValueError("false_alert_budget must be a fraction in (0, 1] (e.g. 0.3 = 30%)")
|
|
222
|
+
return v
|
|
199
223
|
|
|
200
224
|
@field_validator("alert_help_url")
|
|
201
225
|
@classmethod
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";(()=>{var oo={"--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 q(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim()||oo[n]||"#888"}function ro(n){let c=n.replace("#","").trim();c.length===3&&(c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]);let b=parseInt(c,16);return c.length!==6||Number.isNaN(b)?[209,91,54]:[b>>16&255,b>>8&255,b&255]}function H(n,c){let[b,e,w]=ro(n);return`rgba(${b},${e},${w},${c})`}function Vn(n){let c=Math.max(1,window.devicePixelRatio||1),b=n.clientWidth||n.offsetWidth||0,e=n.clientHeight||n.offsetHeight||0;return n.width=Math.round(b*c),n.height=Math.round(e*c),c}var Pn=Number.isFinite;function Nn(n,c,b,e,w,L,U,h,y,z,I,O){let xe=c.length,re=Math.max(1,Math.round(U)),ie=w-e||1,Ge=0;for(let te=0;te<xe;te++){let l=b[te];!Pn(l)||c[te]<e||c[te]>w||Ge++}if(n.strokeStyle=z,n.lineWidth=I*O,n.lineJoin="round",n.beginPath(),Ge<=re){let te=!1;for(let l=0;l<xe;l++){let v=b[l],T=c[l];if(!Pn(v)||T<e||T>w){te=!1;continue}let Z=h(T),me=y(v);te?n.lineTo(Z,me):(n.moveTo(Z,me),te=!0)}}else{let te=new Array(re).fill(null),l=new Array(re).fill(null);for(let T=0;T<xe;T++){let Z=b[T],me=c[T];if(!Pn(Z)||me<e||me>w)continue;let D=Math.floor((me-e)/ie*(re-1));D=D<0?0:D>re-1?re-1:D,(te[D]===null||Z<te[D])&&(te[D]=Z),(l[D]===null||Z>l[D])&&(l[D]=Z)}let v=!1;for(let T=0;T<re;T++){if(l[T]===null){v=!1;continue}let Z=L+T,me=y(l[T]),D=y(te[T]);v?n.lineTo(Z,me):(n.moveTo(Z,me),v=!0),n.lineTo(Z,D)}}n.stroke()}function qn(n){let c=Math.abs(n);return c>=1e3?n.toFixed(0):c>=10?n.toFixed(1):c>=1?n.toFixed(2):n.toFixed(3)}function Un(n,c){let b=new Date(n).toISOString();return c<2*864e5?b.slice(5,16).replace("T"," "):b.slice(5,10)}function Gn(n,c,b){return{left:c.l*b,top:c.t*b,right:n.width-c.r*b,bottom:n.height-c.b*b}}function Yn(n,c,b,e,w,L,U){let h=Gn(c,b,e),y=Math.max(h.left,Math.min(w(L),h.right));y<=h.left+.5||(n.save(),n.fillStyle="rgba(17,15,13,0.42)",n.fillRect(h.left,h.top,y-h.left,h.bottom-h.top),n.strokeStyle=H(q("--faint"),.7),n.lineWidth=1*e,n.setLineDash([4*e,4*e]),n.beginPath(),n.moveTo(y,h.top),n.lineTo(y,h.bottom),n.stroke(),n.setLineDash([]),n.fillStyle=H(q("--faint"),.95),n.font=`${10*e}px ui-monospace, monospace`,n.textAlign="left",n.textBaseline="top",n.fillText(U,y+6*e,h.top+5*e),n.restore())}function Kn(n,c,b,e,w,L,U){let h=Gn(c,b,e),y=5*e;n.save();for(let z=0;z<L.length;z++){let I=L[z],O=w(I.t);if(O<h.left-1||O>h.right+1)continue;let xe=U(I.kind);n.strokeStyle=H(xe,.45),n.lineWidth=1*e,n.beginPath(),n.moveTo(O,h.top),n.lineTo(O,h.bottom),n.stroke(),n.fillStyle=xe,n.beginPath(),n.moveTo(O-y,h.top),n.lineTo(O+y,h.top),n.lineTo(O,h.top+y*1.4),n.closePath(),n.fill()}n.restore()}var io={mad:1,zscore:2,iqr:4};function so(n,c){let b=n.seasonalityData;if(!b||b.length===0)return 0;let e=0;for(let w of c){let L=new Set;for(let U of b)L.add(w.map(h=>{var y;return String((y=U==null?void 0:U[h])!=null?y:"")}).join("|"));e=Math.max(e,L.size)}return e}function kn(n,c){let b=n.timestamps.length;if(c.type==="manual_bounds")return Math.min(c.inputType!=="values"?1:0,b);let e=Math.max(c.minSamples,io[c.type]);if(c.smoothing==="sma"&&(e=Math.max(e,c.smoothingWindow-1)),c.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/c.smoothingAlpha))),c.inputType!=="values"&&(e=Math.max(e,1)),c.seasonalityComponents!==null&&c.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0){let L=so(n,c.seasonalityComponents);if(L>0){let U=c.minSamplesPerGroup*L;c.windowSize>=U&&(e=Math.max(e,U))}}return Math.min(e,b)}var C={l:52,r:14,t:14,b:26},_n=46,lo=10,ao=13,ve=Number.isFinite,Ct=1e3,Tt=60*Ct,At=60*Tt,he=24*At,En=[Ct,2*Ct,5*Ct,10*Ct,15*Ct,30*Ct,Tt,2*Tt,5*Tt,10*Tt,15*Tt,30*Tt,At,2*At,3*At,6*At,12*At,he,2*he,3*he,5*he,7*he,14*he];function Xn(n,c,b){let e=Math.max(c-n,1);if(e>b*14*he){let h=[1,2,3,6,12,24,36,60,120,240],y=h[h.length-1];for(let ie of h)if(e/(ie*30.44*he)<=b){y=ie;break}let z=new Date(n),I=z.getUTCFullYear(),O=z.getUTCMonth();if(y>=12){let ie=y/12;I=Math.floor(I/ie)*ie,O=0}else O=Math.floor(O/y)*y;let xe=[],re=Date.UTC(I,O,1);for(;re<=c;)re>=n&&xe.push(re),O+=y,I+=Math.floor(O/12),O%=12,re=Date.UTC(I,O,1);return{ticks:xe,step:y*30*he}}let w=En[En.length-1];for(let h of En)if(e/h<=b){w=h;break}let L;if(w%he===0){let h=new Date(n);L=Date.UTC(h.getUTCFullYear(),h.getUTCMonth(),h.getUTCDate());let y=w/he,z=Math.round(L/he);L+=(y-z%y)%y*he}else L=Math.ceil(n/w)*w;let U=[];for(let h=L;h<=c;h+=w)h>=n&&U.push(h);return{ticks:U,step:w}}function $n(n,c){let b=new Date(n).toISOString();return c>=320*he?b.slice(0,4):c>=26*he?b.slice(0,7):c>=he?b.slice(5,10):c>=At?b.slice(5,16).replace("T"," "):b.slice(5,19).replace("T"," ")}function Jn(n,c={}){var vn;let b=n.getContext("2d");if(!b)throw new Error("chart: 2D context unavailable");let e=b,w=!!c.navigable,L=w&&c.showNavigator!==!1,U=c.yFit==="data",h=!!c.labeling,y=(vn=c.mode)!=null?vn:"tune",z=()=>h?y:"tune",I=()=>z()!=="label",O=()=>z()==="review"?.3:1,xe=()=>z()==="tune"?1:z()==="review"?.3:.85,re=()=>h&&z()==="label",ie=()=>z()==="label"?1:.55,Ge=()=>z()==="tune",te=()=>h&&(z()==="tune"||z()==="review"),l=1,v=null,T=-1,Z=0,me=!!c.showZeroLine,D=[],N=null,Pe=-1,$=null,Dt=!1,ye=!1,Wt="above",zt=0,Ne=null,He=null,be=null,we=null,_e=null,Se=!1,j=null,V=0,G=1,ke=0,Ee=1,ee=0,de=1,an="";function Ht(){l=Vn(n)}let je=()=>L?(lo+_n)*l:0,Me=()=>n.width-(C.l+C.r)*l,$e=()=>n.height-(C.t+C.b)*l-je(),Re=()=>w?ee:V,Ve=()=>w?de:G,Ie=()=>Ve()-Re()||1,Ye=()=>G-V||1,_=o=>C.l*l+(o-Re())/Ie()*Me(),ce=o=>n.height-C.b*l-je()-(o-ke)/(Ee-ke||1)*$e(),nt=()=>n.height-_n*l,K=()=>(_n-ao)*l,Ke=()=>n.width-(C.l+C.r)*l,le=o=>C.l*l+(o-V)/Ye()*Ke(),Lt=o=>nt()+K()-(o-ke)/(Ee-ke||1)*K(),ot=(o,r,d)=>{let i=new Array(o.length).fill(!1);if(o.length===0)return i;let s=e.measureText($n(o[0],d)).width+16*l,u=-1/0;for(let f=0;f<o.length;f++){let x=r(o[f]);x-u>=s&&(i[f]=!0,u=x)}return i},J=(o,r,d)=>Math.max(r,Math.min(d,o)),rt=()=>{let o=v==null?void 0:v.series,r=o?o.timestamps.length:0,d=r>1?Ye()/(r-1):1e3;return Math.max(d*8,1e3)};function Ce(o,r){var s;let d=r-o,i=rt();if(d<i){let u=(o+r)/2;o=u-i/2,r=u+i/2,d=i}d>=Ye()&&(o=V,r=G),o<V&&(r+=V-o,o=V),r>G&&(o-=r-G,r=G),ee=J(o,V,G),de=J(r,V,G),Dt||(s=c.onViewChange)==null||s.call(c,ee,de),ye&&ne(),W()}function Ot(o,r){w&&(Dt=!0,Ce(o,r),Dt=!1)}let Xe=o=>{let r=(o-C.l*l)/(Me()||1);return Re()+J(r,0,1)*Ie()},Te=o=>{let r=(o-C.l*l)/(Ke()||1);return V+J(r,0,1)*Ye()},qe=o=>{let r=n.height-C.b*l-je(),d=J((r-o)/($e()||1),0,1);return ke+d*(Ee-ke)};function dn(o){let r=v==null?void 0:v.series;if(!r||r.timestamps.length===0)return-1;let d=(o-C.l*l)/(Me()||1),i=Re()+d*Ie(),s=r.timestamps,u=0,f=s.length-1;for(;u<f;){let x=u+f>>1;s[x]<i?u=x+1:f=x}return u>0&&i-s[u-1]<s[u]-i&&(u-=1),u}function Oe(o,r){let d=o.timestamps;if(V=d[0],G=d[d.length-1],w){let f=`${d.length}:${V}:${G}`;f!==an?(an=f,ee=V,de=G):(ee=J(ee,V,G),de=J(Math.max(de,ee+rt()),V,G))}let i=1/0,s=-1/0;for(let f of o.values)ve(f)&&(f<i&&(i=f),f>s&&(s=f));if(!U)for(let f of r)f.scored&&(ve(f.lower)&&f.lower<i&&(i=f.lower),ve(f.upper)&&f.upper>s&&(s=f.upper));(!ve(i)||!ve(s))&&(i=0,s=1),me&&(i>0&&(i=0),s<0&&(s=0)),s<=i&&(s=i+1);let u=(s-i)*.06;ke=i-u,Ee=s+u}function Pt(o,r,d){let i=v.series.timestamps;Nn(e,i,o,Re(),Ve(),C.l*l,Me(),_,ce,r,d,l)}function Bt(o,r){let d=[],i=-1;for(let s=Math.max(0,r);s<o.length;s++){let u=o[s];u.scored&&ve(u.lower)&&ve(u.upper)?i===-1&&(i=s):i!==-1&&(d.push([i,s-1]),i=-1)}return i!==-1&&d.push([i,o.length-1]),d}function In(){if(Z=0,!v||n.width===0||n.height===0)return;let{series:o,scored:r,params:d,alerts:i}=v;if(o.timestamps.length===0){e.fillStyle=q("--term-bg"),e.fillRect(0,0,n.width,n.height);return}let s=q("--clay"),u=q("--st-anomaly"),f=q("--faint"),x=q("--muted");e.fillStyle=q("--term-bg"),e.fillRect(0,0,n.width,n.height),e.font=`${11*l}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let k=0;k<=4;k++){let E=ke+(Ee-ke)*k/4,R=ce(E);e.strokeStyle=H(f,.1),e.lineWidth=1*l,e.beginPath(),e.moveTo(C.l*l,R),e.lineTo(n.width-C.r*l,R),e.stroke(),e.fillStyle=x,e.textAlign="right",e.fillText(qn(E),(C.l-8)*l,R)}if(me&&ke<=0&&Ee>=0){let k=ce(0);e.strokeStyle=H(x,.6),e.lineWidth=1.25*l,e.beginPath(),e.moveTo(C.l*l,k),e.lineTo(n.width-C.r*l,k),e.stroke(),e.fillStyle=x,e.textAlign="right",e.fillText("0",(C.l-8)*l,k)}e.textBaseline="top";let A=n.height-C.b*l-je();if(w){let k=Xn(Re(),Ve(),7),E=ot(k.ticks,_,k.step);for(let R=0;R<k.ticks.length;R++){let se=k.ticks[R],fe=_(se);e.strokeStyle=H(f,.1),e.lineWidth=1*l,e.beginPath(),e.moveTo(fe,C.t*l),e.lineTo(fe,A),e.stroke(),E[R]&&(e.fillStyle=x,e.textAlign=fe<(C.l+24)*l?"left":fe>n.width-(C.r+24)*l?"right":"center",e.fillText($n(se,k.step),fe,A+7*l))}}else{let k=Ie();for(let E=0;E<=5;E++){let R=V+k*E/5,se=_(R);e.fillStyle=x,e.textAlign=E===0?"left":E===5?"right":"center",e.fillText(Un(R,k),se,A+7*l)}}e.save(),e.beginPath(),e.rect(C.l*l,C.t*l,Me(),$e()),e.clip(),vt(),We();let S=r.length,P=Math.min(kn(o,d),S),F=P<S?o.timestamps[P]:void 0,Y=O(),ue=I()?Bt(r,P):[];e.fillStyle=H(s,.13*Y);for(let[k,E]of ue){e.beginPath(),e.moveTo(_(r[k].timestamp),ce(r[k].upper));for(let R=k+1;R<=E;R++)e.lineTo(_(r[R].timestamp),ce(r[R].upper));for(let R=E;R>=k;R--)e.lineTo(_(r[R].timestamp),ce(r[R].lower));e.closePath(),e.fill()}e.strokeStyle=H(s,.4*Y),e.lineWidth=1*l;for(let[k,E]of ue)for(let R of["upper","lower"]){e.beginPath();for(let se=k;se<=E;se++){let fe=_(r[se].timestamp),pt=ce(r[se][R]);se===k?e.moveTo(fe,pt):e.lineTo(fe,pt)}e.stroke()}e.strokeStyle=H(f,.55*Y),e.lineWidth=1*l,e.setLineDash([3*l,3*l]);for(let[k,E]of ue){e.beginPath();for(let R=k;R<=E;R++){let se=r[R].center;if(!ve(se))continue;let fe=_(r[R].timestamp),pt=ce(se);R===k?e.moveTo(fe,pt):e.lineTo(fe,pt)}e.stroke()}if(e.setLineDash([]),d.smoothing!=="none"&&(!h||z()==="tune")){Pt(o.values,H(s,.28),1.25);let k=r.map(E=>E.processedValue);Pt(k,s,1.6)}else Pt(o.values,s,1.5);let X=xe();for(let k=P;k<S;k++){let E=r[k];if(!E.scored||!ve(E.value))continue;let R=_(E.timestamp),se=ce(E.value);E.isAnomaly?(e.fillStyle=H(u,.18*X),e.beginPath(),e.arc(R,se,6*l,0,Math.PI*2),e.fill(),e.fillStyle=H(u,X),e.beginPath(),e.arc(R,se,3*l,0,Math.PI*2),e.fill()):o.truthAnomaly[k]&&(e.strokeStyle=H(x,.7),e.lineWidth=1.25*l,e.beginPath(),e.arc(R,se,3.5*l,0,Math.PI*2),e.stroke())}F!==void 0&&(!h||z()==="tune")&&Yn(e,n,C,l,_,F,"detection at full power \u2192"),T>=0&&T<r.length&&Ge()&&Je(T,d.windowSize,r,o,f),i&&i.length&&Kn(e,n,C,l,_,i,k=>k==="anomaly"?q("--st-anomaly"):k==="anomaly-validated"?q("--st-recovery"):k==="anomaly-false"?q("--st-error"):k==="recovery"?q("--st-recovery"):q("--st-nodata")),un(),wt(r,P,S),e.restore(),L&&cn(o,i,s,f,x)}function cn(o,r,d,i,s){let u=nt(),f=u+K(),x=C.l*l,A=n.width-C.r*l,S=Xn(V,G,5);e.save(),e.beginPath(),e.rect(x,u,Ke(),K()),e.clip(),e.strokeStyle=H(i,.1),e.lineWidth=1*l;for(let X of S.ticks){let k=le(X);e.beginPath(),e.moveTo(k,u),e.lineTo(k,f),e.stroke()}Nn(e,o.timestamps,o.values,V,G,x,Ke(),le,Lt,H(d,.7),1.1,l),e.restore();let P=le(ee),F=le(de);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(x,u,P-x,K()),e.fillRect(F,u,A-F,K()),D.length){let X=q("--st-anomaly");e.fillStyle=H(X,.28);for(let k of D){let E=le(k.start),R=Math.max(le(k.end)-E,2*l);e.fillRect(E,u,R,K())}}if(r&&r.length){let X=q("--st-anomaly");e.fillStyle=H(X,.85);for(let k of r){let E=le(k.t);e.fillRect(E-1*l,u,2*l,K())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(P,u,F-P,K()),e.strokeStyle=d,e.lineWidth=1.5*l,e.strokeRect(P,u+1,F-P,K()-2),e.fillStyle=d;let Y=u+K()/2-8*l;e.fillRect(P-2*l,Y,4*l,16*l),e.fillRect(F-2*l,Y,4*l,16*l),e.font=`${10*l}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=s;let ue=ot(S.ticks,le,S.step);for(let X=0;X<S.ticks.length;X++){let k=S.ticks[X],E=le(k);e.strokeStyle=H(i,.25),e.lineWidth=1*l,e.beginPath(),e.moveTo(E,f),e.lineTo(E,f+3*l),e.stroke(),ue[X]&&(e.textAlign=E<(C.l+26)*l?"left":E>n.width-(C.r+26)*l?"right":"center",e.fillText($n(k,S.step),E,f+5*l))}}function Je(o,r,d,i,s){let u=i.timestamps,f=Math.max(0,o-r),x=o-1,A=C.t*l,S=$e();if(x>=f){let Y=i.intervalSeconds*1e3/Ie()*Me()*.5,ue=_(u[f])-Y,X=_(u[x])+Y;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(ue,A,X-ue,S),e.strokeStyle=H(s,.5),e.lineWidth=1*l,e.beginPath(),e.moveTo(ue,A),e.lineTo(ue,A+S),e.moveTo(X,A),e.lineTo(X,A+S),e.stroke()}let P=_(u[o]);e.strokeStyle=H(s,.85),e.lineWidth=1*l,e.setLineDash([2*l,2*l]),e.beginPath(),e.moveTo(P,A),e.lineTo(P,A+S),e.stroke(),e.setLineDash([]);let F=d[o];if(F.scored&&ve(F.lower)&&ve(F.upper)){let Y=5*l;e.strokeStyle=H(q("--clay"),.85),e.lineWidth=1.5*l;for(let ue of[F.lower,F.upper]){let X=ce(ue);e.beginPath(),e.moveTo(P-Y,X),e.lineTo(P+Y,X),e.stroke()}}if(ve(F.value)){let Y=ce(F.value);e.fillStyle=q("--term-bg"),e.beginPath(),e.arc(P,Y,4*l,0,Math.PI*2),e.fill(),e.strokeStyle=F.isAnomaly?q("--st-anomaly"):q("--clay"),e.lineWidth=2*l,e.beginPath(),e.arc(P,Y,4*l,0,Math.PI*2),e.stroke()}}function Ft(o,r,d,i,s){e.beginPath(),e.moveTo(o+s,r),e.arcTo(o+d,r,o+d,r+i,s),e.arcTo(o+d,r+i,o,r+i,s),e.arcTo(o,r+i,o,r,s),e.arcTo(o,r,o+d,r,s),e.closePath()}function bt(o,r){let d=q("--st-anomaly"),i=14*l,s=3*l,u=o-i-s,f=C.t*l+s;e.fillStyle=r?H(d,.95):"rgba(27,25,22,0.82)",e.strokeStyle=H(d,.9),e.lineWidth=1*l,Ft(u,f,i,i,3*l),e.fill(),e.stroke(),e.strokeStyle=r?"#fff":d,e.lineWidth=1.5*l;let x=4*l;e.beginPath(),e.moveTo(u+x,f+x),e.lineTo(u+i-x,f+i-x),e.moveTo(u+i-x,f+x),e.lineTo(u+x,f+i-x),e.stroke()}let Nt=()=>Ne!=null?Ne:He,jt=()=>{let o=_e||be;return o?[Math.min(o.a,o.b),Math.max(o.a,o.b)]:[Re(),Ve()]};function gt(){let o=[],r=Nt();if(r==null||!v)return o;let d=v.series.timestamps,i=v.series.values,[s,u]=jt(),f=-1,x=-1,A=0;for(let S=0;S<d.length;S++){if(d[S]<s||d[S]>u)continue;let P=i[S];ve(P)&&(Wt==="above"?P>r:P<r)?(f===-1&&(f=d[S]),x=d[S],A=0):f!==-1&&(A++,A>zt&&(o.push([f,x]),f=-1,A=0))}return f!==-1&&o.push([f,x]),o}function ne(){if(!c.onThresholdChange)return;let[o,r]=jt(),d=_e||be;c.onThresholdChange({value:Nt(),locked:Ne!=null,runs:gt().length,window:d?{start:Math.min(d.a,d.b),end:Math.max(d.a,d.b)}:null,committed:be!=null,windowMs:r-o})}function it(o,r){let d=null;for(let i=D.length-1;i>=0;i--){let s=D[i];o<=s.end&&r>=s.start&&(d===null?(s.start=Math.min(s.start,o),s.end=Math.max(s.end,r),d=s):(d.start=Math.min(d.start,s.start),d.end=Math.max(d.end,s.end),N===s&&(N=d),D.splice(i,1)))}d===null&&D.push({start:o,end:r,label:""})}function vt(){if(!ye||Nt()==null)return;let r=C.t*l,d=$e(),i=q("--st-nodata");for(let[s,u]of gt()){let f=_(s),x=Math.max(_(u)-f,2*l);e.fillStyle=H(i,.22),e.fillRect(f,r,x,d),e.strokeStyle=H(i,.6),e.lineWidth=1*l,e.strokeRect(f,r,x,d)}}function un(){if(!ye)return;let o=C.t*l,r=n.height-C.b*l-je(),d=C.l*l,i=n.width-C.r*l,s=q("--st-nodata"),[u,f]=jt(),x=!!(_e||be),A=J(_(u),d,i),S=J(_(f),d,i);x&&(e.fillStyle=H(q("--ink"),.5),e.fillRect(d,o,Math.max(0,A-d),$e()),e.fillRect(S,o,Math.max(0,i-S),$e()),e.strokeStyle=H(s,.7),e.lineWidth=1*l,e.setLineDash([3*l,3*l]),e.beginPath(),e.moveTo(A,o),e.lineTo(A,r),e.moveTo(S,o),e.lineTo(S,r),e.stroke(),e.setLineDash([]));let P=Nt();if(P!=null&&P>=ke&&P<=Ee){let F=ce(P);e.strokeStyle=s,e.lineWidth=1.5*l,e.setLineDash([6*l,4*l]),e.beginPath(),e.moveTo(A,F),e.lineTo(S,F),e.stroke(),e.setLineDash([])}}let Vt=()=>v?v.series.intervalSeconds*1e3:1e3;function oe(o,r,d){let i=!1;for(let s=0,u=d.length-1;s<d.length;u=s++){let f=d[s].x,x=d[s].y,A=d[u].x,S=d[u].y;x>r!=S>r&&o<(A-f)*(r-x)/(S-x||1e-9)+f&&(i=!i)}return i}function st(o,r,d){let i=[];if(!j||j.length<3)return i;for(let s=Math.max(0,r);s<d;s++){let u=o[s];!u.scored||!u.isAnomaly||!ve(u.value)||oe(_(u.timestamp),ce(u.value),j)&&i.push(s)}return i}function xt(o,r){var S;if(!r.length)return[];let d=Vt(),s=(Math.max(1,(S=v==null?void 0:v.params.consecutiveAnomalies)!=null?S:1)+1)*d,u=d/2,f=[],x=r[0],A=r[0];for(let P=1;P<r.length;P++){let F=r[P];o[F].timestamp-o[A].timestamp>s&&(f.push([o[x].timestamp-u,o[A].timestamp+u]),x=F),A=F}return f.push([o[x].timestamp-u,o[A].timestamp+u]),f}function qt(o,r,d){if(!c.onLassoChange)return;let i=Se&&j?st(o,r,d):[];c.onLassoChange({active:!!j,anomalies:i.length,incidents:xt(o,i).length})}function yt(){if(!v||!j)return;let{scored:o}=v,r=o.length,d=Math.min(kn(v.series,v.params),r),i=xt(o,st(o,d,r));for(let[s,u]of i)it(s,u);i.length&&ze()}function wt(o,r,d){if(!Se||!j||j.length<2)return;let i=q("--clay"),s=q("--st-anomaly");e.beginPath(),e.moveTo(j[0].x,j[0].y);for(let u=1;u<j.length;u++)e.lineTo(j[u].x,j[u].y);e.closePath(),e.fillStyle=H(i,.08),e.fill(),e.strokeStyle=H(i,.9),e.lineWidth=1.5*l,e.setLineDash([5*l,4*l]),e.stroke(),e.setLineDash([]);for(let u of st(o,r,d)){let f=_(o[u].timestamp),x=ce(o[u].value);e.strokeStyle=s,e.lineWidth=2*l,e.beginPath(),e.arc(f,x,7*l,0,Math.PI*2),e.stroke()}}function We(){let o=C.t*l,r=$e(),d=q("--st-anomaly"),i=C.l*l,s=n.width-C.r*l,u=re(),f=ie();for(let x=0;x<D.length;x++){let A=D[x],S=_(A.start),P=_(A.end);if(P<i-1||S>s+1)continue;let F=Math.max(P-S,2*l),Y=u&&A===N;e.fillStyle=H(d,(Y?.3:.16)*f),e.fillRect(S,o,F,r),e.strokeStyle=H(d,(Y?.95:.5)*f),e.lineWidth=(Y?2:1)*l,e.strokeRect(S,o,F,r),u&&(e.fillStyle=H(d,.95),e.fillRect(S-1.5*l,o,3*l,r),e.fillRect(P-1.5*l,o,3*l,r),(P-S>=22*l||Y)&&bt(P,Y||x===Pe))}if(u&&$&&$.mode==="new"){let x=_($.a),A=_($.b);e.fillStyle=H(q("--st-nodata"),.28),e.fillRect(Math.min(x,A),o,Math.abs(A-x),r)}}function lt(o,r){if(!v||!v.alerts||!v.alerts.length||r>(C.t+22)*l)return-1;let d=-1,i=9*l;for(let s=0;s<v.alerts.length;s++){let u=Math.abs(o-_(v.alerts[s].t));u<=i&&(i=u,d=s)}return d}function Ze(o){var i;if(!v||!v.alerts)return;let r=v.alerts[o].kind,d=r==="anomaly"?"valid":r==="anomaly-validated"?"false":"unreviewed";(i=c.onAlertReviewChange)==null||i.call(c,v.alerts[o].t,d)}function ze(){var o;(o=c.onIncidentsChange)==null||o.call(c,D)}function Ut(o){let r=D.indexOf(o);r<0||(D.splice(r,1),N===o&&(N=null),Pe=-1,ze(),W())}function at(o,r){let d=C.t*l,i=6*l;for(let s=0;s<D.length;s++){let u=_(D[s].end);if(u-_(D[s].start)>=22*l||D[s]===N){let f=14*l,x=3*l,A=u-f-x,S=d+x;if(o>=A&&o<=A+f&&r>=S&&r<=S+f)return{i:s,edge:"del"}}}for(let s=0;s<D.length;s++){let u=_(D[s].start),f=_(D[s].end);if(Math.abs(o-u)<=i)return{i:s,edge:"a"};if(Math.abs(o-f)<=i)return{i:s,edge:"b"}}for(let s=0;s<D.length;s++){let u=_(D[s].start),f=_(D[s].end);if(o>u+i&&o<f-i)return{i:s,edge:"move"}}return null}let dt=()=>{let o=v==null?void 0:v.series,r=o?o.timestamps.length:0;return r>1?Math.max(Ye()/(r-1),1):1e3};function W(){Z===0&&(Z=requestAnimationFrame(In))}function Be(){if(!v)return;let o=v.scored.length,r=Math.min(kn(v.series,v.params),o);qt(v.scored,r,o)}function Ae(){var d;if(!c.onHover||!v)return;if(T<0){c.onHover(null);return}let o=(d=v.scored[T])!=null?d:null,r={index:T,point:o,windowStart:Math.max(0,T-v.params.windowSize),windowEnd:T-1};c.onHover(r)}let De=null,ae=null,Ue=o=>{let r=n.getBoundingClientRect();return{x:(o.clientX-r.left)*l,y:(o.clientY-r.top)*l}},Gt=o=>L&&o>=nt(),_t=o=>{let r=le(ee),d=le(de),i=8*l;return Math.abs(o-r)<=i?"l":Math.abs(o-d)<=i?"r":o>r&&o<d?"move":"out"};function mn(o){if(!v)return;let{x:r,y:d}=Ue(o);if(De||ae||$)return;if(Gt(d)){T!==-1&&(T=-1,Ae(),W());let u=_t(r);n.style.cursor=u==="l"||u==="r"?"ew-resize":u==="move"?"grab":"pointer";return}let i=z();if(h&&i==="label"&&Se){T!==-1&&(T=-1,Ae()),n.style.cursor="crosshair";return}if(h&&i==="label"&&ye){T!==-1&&(T=-1,Ae()),Ne==null&&!we&&(He=qe(d),ne(),W()),n.style.cursor="crosshair";return}if(h&&i==="label"){T!==-1&&(T=-1,Ae());let u=at(r,d),f=u&&u.edge==="del"?u.i:-1;f!==Pe&&(Pe=f,W()),n.style.cursor=u?u.edge==="del"?"pointer":u.edge==="move"?"grab":"ew-resize":"crosshair";return}if(h&&i==="review"){T!==-1&&(T=-1,Ae()),n.style.cursor=lt(r,d)>=0?"pointer":"grab";return}w&&(n.style.cursor=h&<(r,d)>=0?"pointer":"grab");let s=dn(r);s!==T&&(T=s,Ae(),W())}function Yt(){T!==-1&&(T=-1,Ae(),W())}function kt(o){if(!w||!v)return;let{x:r,y:d}=Ue(o);if(L&&d>=nt()){let i=_t(r);if(i==="l")ae={type:"l",grab:0,vMin:ee,vMax:de};else if(i==="r")ae={type:"r",grab:0,vMin:ee,vMax:de};else if(i==="move")ae={type:"move",grab:Te(r),vMin:ee,vMax:de};else{let s=Te(r),u=Ie();Ce(s-u/2,s+u/2),ae={type:"move",grab:s,vMin:ee,vMax:de}}n.style.cursor="grabbing",o.preventDefault();return}if(d>C.t*l&&d<n.height-C.b*l-je()){let i=z();if(te()){let s=lt(r,d);if(s>=0){Ze(s),o.preventDefault();return}}if(h&&i==="label"&&Se){j=[{x:r,y:d}],Be(),n.style.cursor="crosshair",W(),o.preventDefault();return}if(h&&i==="label"&&ye){we={x:r,ts:Xe(r)},Ne==null&&(He=qe(d)),ne(),W(),o.preventDefault();return}if(h&&i==="label"){let s=Xe(r),u=at(r,d);if(u&&u.edge==="del"){Ut(D[u.i]),o.preventDefault();return}if(u&&u.edge==="move"){let f=D[u.i];N=f,$={mode:"move",iv:f,grab:s,a0:f.start,b0:f.end,moved:!1}}else if(u){let f=D[u.i];N=f,$={mode:"edge",iv:f,edge:u.edge,moved:!1}}else N=null,$={mode:"new",a:s,b:s,moved:!1};n.style.cursor="crosshair",W(),o.preventDefault();return}De={x:r,vMin:ee,vMax:de},n.style.cursor="grabbing",o.preventDefault()}}function Mt(o){if(!v)return;let{x:r,y:d}=Ue(o);if(Se&&j){j.push({x:r,y:d}),Be(),W();return}if(ye&&we){Math.abs(r-we.x)>6*l?_e={a:we.ts,b:Xe(r)}:(_e=null,Ne==null&&(He=qe(d))),ne(),W();return}if($){let i=Xe(r);if($.mode==="new")$.b=i,Math.abs(_($.b)-_($.a))>4*l&&($.moved=!0);else if($.mode==="edge"){let s=$.iv,u=dt();$.edge==="a"?s.start=J(Math.min(i,s.end-u),V,G):s.end=J(Math.max(i,s.start+u),V,G),$.moved=!0}else{let s=$.iv,u=$.a0+(i-$.grab),f=$.b0+(i-$.grab);u<V&&(f+=V-u,u=V),f>G&&(u-=f-G,f=G),s.start=J(u,V,G),s.end=J(f,V,G),$.moved=!0}W();return}if(De){let i=(r-De.x)*(De.vMax-De.vMin)/(Me()||1);Ce(De.vMin-i,De.vMax-i)}else if(ae){let i=Te(r);ae.type==="l"?Ce(Math.min(i,de-rt()),de):ae.type==="r"?Ce(ee,Math.max(i,ee+rt())):Ce(ae.vMin+(i-ae.grab),ae.vMax+(i-ae.grab))}}function Et(o){if(Se&&j){yt(),j=null,Be(),W();return}if(ye&&we){let{x:r,y:d}=Ue(o);if(Math.abs(r-we.x)>6*l){let i=we.ts,s=Xe(r);be={a:Math.min(i,s),b:Math.max(i,s)}}else Ne=qe(d);we=null,_e=null,ne(),W();return}if($){let r=$;if(r.mode==="new")if(r.moved){let d=J(Math.min(r.a,r.b),V,G),i=J(Math.max(r.a,r.b),V,G),s={start:d,end:i,label:""};D.push(s),N=s}else N=null;else if(r.iv.start>r.iv.end){let d=r.iv.start;r.iv.start=r.iv.end,r.iv.end=d}$=null,n.style.cursor="crosshair",r.moved&&ze(),W();return}(De||ae)&&(De=null,ae=null,n.style.cursor="grab")}function ct(o){let r=o.target,d=!!r&&(r.tagName==="INPUT"||r.tagName==="TEXTAREA"||r.isContentEditable);if(Se){o.key==="Escape"&&j&&(j=null,Be(),W());return}ye||d||!N||(o.key==="Delete"||o.key==="Backspace"?(o.preventDefault(),Ut(N)):o.key==="Escape"&&(N=null,W()))}function ut(o){if(!w||!v)return;if(Se&&j){o.preventDefault();return}o.preventDefault();let{x:r,y:d}=Ue(o),i=J(Ie()*Math.pow(1.0015,o.deltaY),rt(),Ye());if(d>=nt()){let s=Te(r);Ce(s-i/2,s+i/2)}else{let s=Xe(r),u=(s-ee)/(Ie()||1);Ce(s-u*i,s-u*i+i)}}function mt(){w&&Ce(V,G)}n.addEventListener("mousemove",mn),n.addEventListener("mouseleave",Yt),w&&(n.addEventListener("mousedown",kt),n.addEventListener("wheel",ut,{passive:!1}),n.addEventListener("dblclick",mt),window.addEventListener("mousemove",Mt),window.addEventListener("mouseup",Et)),h&&(window.addEventListener("keydown",ct),n.style.cursor=y==="tune"?"grab":"crosshair");function Kt(o){v=o,!h&&o.incidents&&(D=o.incidents),Oe(o.series,o.scored),T>=o.series.timestamps.length&&(T=-1),W()}function ft(o){me!==o&&(me=o,v&&Oe(v.series,v.scored),W())}function fn(o){D=o,N=null,Pe=-1,W()}function pn(o){h&&(ye=o,o?(N=null,Se=!1,j=null,Be()):(He=null,we=null,_e=null),n.style.cursor="crosshair",ne(),W())}function hn(o){h&&(Se=o,o?(ye=!1,we=null,_e=null,He=null,N=null,ne()):j=null,n.style.cursor="crosshair",Be(),W())}function Mn(o){!h||y===o||(y=o,o!=="label"&&(ye=!1,we=null,_e=null,He=null,Se=!1,j=null,N=null,Pe=-1,ne(),Be()),T=-1,n.style.cursor=o==="tune"?"grab":"crosshair",v&&Oe(v.series,v.scored),W())}function Xt(o){Wt=o,ne(),W()}function Jt(o){zt=Math.max(0,Math.floor(o)||0),ne(),W()}function ge(o){Ne=o!=null&&ve(o)?o:null,ne(),W()}function $t(){let o=gt(),r=Vt()/2;for(let[d,i]of o)it(d-r,i+r);return o.length&&ze(),ne(),W(),o.length}function Zt(){be=null,_e=null,ne(),W()}function Qt(){return be?{start:Math.min(be.a,be.b),end:Math.max(be.a,be.b)}:null}function St(o){be=o?{a:o.start,b:o.end}:null,ne(),W()}function bn(){Ht(),v&&(Oe(v.series,v.scored),W())}function gn(){n.removeEventListener("mousemove",mn),n.removeEventListener("mouseleave",Yt),w&&(n.removeEventListener("mousedown",kt),n.removeEventListener("wheel",ut),n.removeEventListener("dblclick",mt),window.removeEventListener("mousemove",Mt),window.removeEventListener("mouseup",Et)),h&&window.removeEventListener("keydown",ct),Z!==0&&cancelAnimationFrame(Z),Z=0,v=null}return Ht(),{render:Kt,resize:bn,setMode:Mn,setZeroLine:ft,setViewWindow:Ot,setIncidents:fn,setThresholdMode:pn,setThresholdDirection:Xt,setThresholdGap:Jt,setThresholdValue:ge,applyThreshold:$t,setLassoMode:hn,clearCaptureWindow:Zt,getCaptureWindow:Qt,setCaptureWindow:St,destroy:gn}}var co={mad:3,zscore:3,iqr:1.5},uo={mad:10,zscore:3,iqr:4},mo="dtk-tune";function m(n,c,b){let e=document.createElement(n);return c&&(e.className=c),b!=null&&(e.textContent=b),e}function ln(n,c){let b=m("label","dtk-ctl-label",n);if(c){b.title=c;let e=m("span","dtk-ctl-info","\u24D8");e.title=c,b.appendChild(document.createTextNode(" ")),b.appendChild(e)}return b}function on(n,c,b,e,w){let L=m("div","dtk-ctl");L.appendChild(ln(n,w));let U=m("div","dtk-seg"),h=b,y=[],z=()=>{y.forEach(I=>I.classList.toggle("on",I.dataset.v===h))};return c.forEach(I=>{let O=m("button","dtk-seg-btn",I.label);O.type="button",O.dataset.v=I.value,O.onclick=()=>{h=I.value,z(),e(h)},y.push(O),U.appendChild(O)}),z(),L.appendChild(U),{row:L,get:()=>h,set:I=>{h=I,z()}}}function It(n,c,b){var z;let e=m("div","dtk-ctl"),w=m("div","dtk-ctl-head"),L=ln(n,c.hint),U=m("span","dtk-ctl-val"),h=(z=c.fmt)!=null?z:(I=>String(I));w.appendChild(L),w.appendChild(U),e.appendChild(w);let y=m("input","dtk-range");return y.type="range",y.min=String(c.min),y.max=String(c.max),y.step=String(c.step),y.value=String(c.value),U.textContent=h(c.value),y.oninput=()=>{let I=Number(y.value);U.textContent=h(I),b(I)},e.appendChild(y),{row:e,get:()=>Number(y.value),setMax:I=>{y.max=String(I),Number(y.value)>I&&(y.value=String(I),U.textContent=h(I))}}}function Qn(n){if(n.type==="manual_bounds"){let b={};return n.lowerBound!=null&&(b.lower_bound=n.lowerBound),n.upperBound!=null&&(b.upper_bound=n.upperBound),n.inputType!=="values"&&(b.input_type=n.inputType),b}let c={threshold:n.threshold,window_size:n.windowSize};return n.windowWeights!=="none"&&(c.window_weights=n.windowWeights,n.windowWeights==="exponential"&&n.halfLife!=null&&(c.half_life=n.halfLife)),n.detrend!=="none"&&(c.detrend=n.detrend),n.smoothing!=="none"&&(c.smoothing=n.smoothing),n.inputType!=="values"&&(c.input_type=n.inputType),n.seasonalityComponents&&n.seasonalityComponents.length&&(c.seasonality_components=n.seasonalityComponents,c.min_samples_per_group=n.minSamplesPerGroup),c}function fo(n,c){let b=Qn(n),e=[`type: ${n.type}`];for(let[w,L]of Object.entries(b))e.push(`${w}=${typeof L=="object"?JSON.stringify(L):L}`);return n.direction&&n.direction!=="any"&&e.push(`direction=${n.direction}`),e.push(`consecutive_anomalies=${c}`),e.join(" \xB7 ")}function po(n,c){var On,Bn,Fn;bo(),c.classList.add(mo),c.innerHTML="";let b=m("div","dtk-tune-root");c.appendChild(b);let e=n.points.length,w={timestamps:n.points.map(t=>t.t),values:n.points.map(t=>t.v==null?NaN:t.v),intervalSeconds:n.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:n.seasonality_columns.length?n.seasonality:void 0,seasonalityColumns:n.seasonality_columns.length?n.seasonality_columns:void 0},L=w,U=t=>{let a=Math.max(0,e-t);return a<=0?w:{timestamps:w.timestamps.slice(a),values:w.values.slice(a),intervalSeconds:w.intervalSeconds,truthAnomaly:w.truthAnomaly.slice(a),seasonalityData:w.seasonalityData?w.seasonalityData.slice(a):void 0,seasonalityColumns:w.seasonalityColumns}},h=t=>Date.parse(t.replace(" ","T")+"Z"),y=(n.incidents||[]).map(t=>({start:h(t.start),end:h(t.end),label:t.label||""})).filter(t=>Number.isFinite(t.start)&&Number.isFinite(t.end)).map(t=>({start:Math.min(t.start,t.end),end:Math.max(t.start,t.end),label:t.label})),z=(()=>{let t=(n.capture_windows||[])[0];if(!t)return null;let a=h(t.start),p=h(t.end);return!Number.isFinite(a)||!Number.isFinite(p)?null:{start:Math.min(a,p),end:Math.max(a,p)}})(),I=[],O=[],xe=[],re=[],ie=(n.alert_reviews||[]).map(t=>({start:h(t.start),end:h(t.end),verdict:t.verdict==="false"?"false":"valid"})).filter(t=>Number.isFinite(t.start)&&Number.isFinite(t.end)),Ge=()=>n.interval_seconds*1e3/2,te=(t,a)=>{let p=Ge();for(let M of ie)if(a>=M.start-p&&t<=M.end+p)return M.verdict;return null},l=(t,a,p)=>{let M=Ge();for(let g=ie.length-1;g>=0;g--){let B=ie[g];a>=B.start-M&&t<=B.end+M&&ie.splice(g,1)}p&&ie.push({start:t,end:a,verdict:p})},v=(t,a)=>{let p=Ge();return t.end>=a.start-p&&t.start<=a.end+p},T=()=>ie.filter(t=>t.verdict==="valid").map(t=>({start:t.start,end:t.end,label:"confirmed alert"})),Z=()=>T().filter(t=>!y.some(a=>v(t,a))),me=()=>[...y,...Z()],D=()=>I.map((t,a)=>{var g;let p=(g=O[a])!=null?g:[t,t],M=te(p[0],p[1]);return{t,kind:M==="valid"?"anomaly-validated":M==="false"?"anomaly-false":"anomaly"}}),N=n.detector,Pe=n.consecutive_anomalies,$=w.values.filter(t=>Number.isFinite(t)).sort((t,a)=>t-a),Dt=$.length?$[0]:0,ye=$.length?$[$.length-1]:1,Wt=t=>$.length?$[Math.min($.length-1,Math.max(0,Math.round(t*($.length-1))))]:0,zt=Math.max((ye-Dt)*.05,1e-9),Ne=Dt-zt,He=ye+zt,be=Math.max((He-Ne)/400,1e-9),we=N.lowerBound!=null?N.lowerBound:Wt(.05),_e=N.upperBound!=null?N.upperBound:Wt(.95),Se=(On=N.seasonalityComponents)!=null?On:[],j=new Map;Se.forEach((t,a)=>t.forEach(p=>j.set(p,a+1)));let V=()=>{let t=0;j.forEach(p=>{p>t&&(t=p)});let a=[];for(let p=1;p<=t;p++){let M=n.seasonality_columns.filter(g=>j.get(g)===p);M.length&&a.push(M)}return a.length?a:null},G=t=>{if(!t||!n.seasonality.length)return 0;let a=0;for(let p of t){let M=new Set;for(let g of n.seasonality)M.add(p.map(B=>{var Q;return String((Q=g==null?void 0:g[B])!=null?Q:"")}).join("|"));a=Math.max(a,M.size)}return a},ke=()=>{var t;return{type:$t.get(),threshold:St.get(),windowSize:d.get(),minSamples:N.minSamples,inputType:N.inputType,smoothing:x.get(),smoothingAlpha:N.smoothingAlpha,smoothingWindow:N.smoothingWindow,windowWeights:i.get(),halfLife:i.get()==="exponential"?s.get():null,detrend:f.get(),seasonalityComponents:V(),minSamplesPerGroup:(t=uo[$t.get()])!=null?t:N.minSamplesPerGroup,consecutiveAnomalies:Pe,direction:S.get(),lowerBound:Zt.get(),upperBound:Qt.get()}},Ee=m("div","dtk-tune-header"),ee=m("div","dtk-tune-titlerow");ee.appendChild(m("h1","dtk-tune-title",n.metric));let de=m("span","dtk-tune-badge",n.save_url?"manual tuning":"preview");ee.appendChild(de),Ee.appendChild(ee);let an=n.project?`${n.project} \xB7 `:"";Ee.appendChild(m("div","dtk-tune-sub",`${an}${e} points \xB7 ${ho(n.interval_seconds)} grid`)),n.description&&Ee.appendChild(m("div","dtk-tune-desc",n.description)),b.appendChild(Ee);let Ht=m("div","dtk-tune-metrics"),je=m("div","dtk-tune-cockpit");b.appendChild(je);let Me=m("div","dtk-tune-stage");je.appendChild(Me);let $e=m("div","dtk-tune-hud");$e.appendChild(Ht),Me.appendChild($e);let Re=m("div","dtk-tune-stagefoot"),Ve=m("div","dtk-tune-rail");je.appendChild(Ve);let Ie=m("div","dtk-tune-railhead"),Ye=m("span","dtk-rail-title","Tune \xB7 controls"),_=m("button","dtk-dock-toggle","\u27E9");_.type="button",_.title="Collapse the control rail to give the chart the whole width.",Ie.appendChild(Ye),Ie.appendChild(_),Ve.appendChild(Ie);let ce=m("div","dtk-tune-controls");Ve.appendChild(ce);let nt=m("div","dtk-rail-group"),K=m("div","dtk-rail-group"),Ke=m("div","dtk-rail-group"),le=m("div","dtk-rail-group"),Lt=m("div","dtk-rail-group");Ke.style.display="none",le.style.display="none",ce.append(nt,K,Ke,le,Lt);let ot=m("div","dtk-tune-railfoot");Ve.appendChild(ot);let J=m("button","dtk-rail-open","\u2699");J.type="button",J.title="Show the control rail",J.style.display="none",Me.appendChild(J);let rt=t=>{Ve.style.display=t?"":"none",J.style.display=t?"none":""};_.onclick=()=>rt(!1),J.onclick=()=>rt(!0);let Ce=m("div","dtk-tune-trim"),Ot=m("div","dtk-tune-trim-head");Ot.appendChild(ln("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 Xe=m("span","dtk-tune-trim-val");Ot.appendChild(Xe),Ce.appendChild(Ot);let Te=m("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),Ce.appendChild(Te),nt.appendChild(Ce);let qe=m("div","dtk-tune-chart"),dn=m("canvas");qe.appendChild(dn);let Oe=m("div","dtk-tune-spin");Oe.appendChild(m("span","dtk-spin-ring")),Oe.appendChild(m("span","dtk-spin-txt","computing\u2026")),qe.appendChild(Oe),Me.appendChild(qe),Me.appendChild(Re);let Pt=m("div","dtk-tune-modes"),Bt={};[{v:"tune",label:"Tune",hint:"Steer the band \u2014 the confidence corridor leads; incidents recede to read-only context. Hover a point for its window."},{v:"review",label:"Review alerts",hint:"Confirm the fired alerts \u2014 click a marker to cycle un-reviewed \u2192 valid (green) \u2192 false (slate). The band ghosts so the alerts lead."},{v:"label",label:"Label incidents",hint:"Mark real incidents \u2014 drag a span, lasso the anomaly cloud, or threshold-capture. The band hides so incidents lead."}].forEach(t=>{let a=m("button","dtk-mode-btn",t.label);a.type="button",a.title=t.hint,a.dataset.v=t.v,a.onclick=()=>Yt(t.v),Bt[t.v]=a,Pt.appendChild(a)}),$e.appendChild(Pt);let cn=m("div","dtk-tune-legend"),Je=(t,a,p)=>{let M=m("span","dtk-leg-item");M.title=p,M.appendChild(m("span",`dtk-leg-sw ${t}`)),M.appendChild(m("span","dtk-leg-txt",a)),cn.appendChild(M)};Je("alert","alert","A fired alert, not yet reviewed \u2014 enough consecutive anomalies to meet the rule."),Je("alert-ok","valid alert","An alert you confirmed is real (click a marker in Review mode). Counts toward recall."),Je("alert-no","false alarm","An alert you marked a false positive. Stays in the false-alert rate."),Je("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),Je("line","metric","The metric value over time."),Je("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),Je("center","band center","The expected value at the middle of the band."),Me.insertBefore(cn,qe);let Ft=m("div","dtk-tune-readout");Re.appendChild(Ft);let bt=m("div","dtk-tune-warn");bt.style.display="none",Re.appendChild(bt);let Nt=t=>{let a=t.seasonalityComponents,p=G(a),M=t.minSamplesPerGroup*p;a&&p>0&&t.windowSize<M?(bt.textContent=`\u26A0 Seasonality inactive at this window: ${t.windowSize} < ${M} (min_samples_per_group ${t.minSamplesPerGroup} \xD7 ${p} key${p===1?"":"s"}). Each point keeps only ~${Math.floor(t.windowSize/p)} same-key point(s), so the band falls back to global statistics (seasonality has no effect). Raise the window to \u2265 ${M}.`,bt.style.display=""):bt.style.display="none"},jt=t=>{let a=Ge(),p=L.timestamps,M=(p.length?p[0]:0)-a,g=(p.length?p[p.length-1]:0)+a,B=Le=>Le.end>=M&&Le.start<=g,Q=y.filter(B),ht=T().filter(B).filter(Le=>!Q.some(Fe=>v(Le,Fe))),et=[...Q,...ht],tn=(Le,Fe)=>Le[1]>=Fe.start-a&&Le[0]<=Fe.end+a,pe=0,tt=0,jn=0;for(let Le of t){let Fe=te(Le[0],Le[1]);Fe&&jn++,Fe==="valid"&&tt++,(Fe==="valid"||Fe!=="false"&&et.some(no=>tn(Le,no)))&&pe++}let Ln=0;for(let Le of et)t.some(Fe=>tn(Fe,Le))&&Ln++;let nn=t.length;return{realIncidents:et.length,caught:Ln,recall:et.length?Ln/et.length:NaN,totalAlerts:nn,correctAlerts:pe,falseAlerts:nn-pe,fdr:nn?(nn-pe)/nn:NaN,validated:tt,reviewed:jn}},gt=t=>Number.isFinite(t)?`${Math.round(t*100)}%`:"\u2014",ne=typeof n.false_alert_budget=="number"&&n.false_alert_budget>0?n.false_alert_budget:null,it=(t,a,p,M,g)=>`<span class="dtk-m-chip${g!=null&&g.cls?" "+g.cls:""}"${g!=null&&g.title?` title="${g.title}"`:""}><span class="dtk-m-dot" style="background:${M}"></span><span class="dtk-m-v">${t}</span><span class="dtk-m-l">${a}</span>`+(p?`<span class="dtk-m-sub">${p}</span>`:"")+"</span>";function vt(){let t=jt(O),a=t.realIncidents>0,p=ne!=null?`${Math.round(ne*100)}%`:"",M=a&&t.totalAlerts>0&&ne!=null&&Number.isFinite(t.fdr)&&t.fdr>ne,g;if(!a)g="mark incidents to measure";else if(t.totalAlerts===0)g="no alerts";else if(t.falseAlerts===0)g=`${gt(t.fdr)} \xB7 all correct`;else{let ht=t.totalAlerts/t.falseAlerts,et=ht>=9.5?String(Math.round(ht)):String(Math.round(ht*10)/10);g=`${gt(t.fdr)} \xB7 \u22481 in ${et} false`}M&&(g+=` \xB7 \u25B2 over ${p} budget`);let B=t.totalAlerts===0?"no alerts":`${t.validated} valid${t.totalAlerts>t.reviewed?` \xB7 ${t.totalAlerts-t.reviewed} left`:" \xB7 all reviewed"}`,Q=ne?`Budget: at most ${p} of fired alerts false (false-alert rate / FDR). Set per metric or project via false_alert_budget. Labeling is optional \u2014 this just flags when you exceed the budget.`:"";Ht.innerHTML=it(String(t.realIncidents),"real incidents","","var(--c)")+it(a?`${t.caught}/${t.realIncidents}`:"\u2014","caught",a?`recall ${gt(t.recall)}`:"mark or confirm","var(--green)")+it(String(t.totalAlerts),"alerts","","var(--c)")+it(a?String(t.falseAlerts):"\u2014","false alerts",g,"var(--anom)",{cls:M?"over":"",title:Q})+it(`${t.reviewed}/${t.totalAlerts}`,"reviewed",B,"var(--green)")}let un=()=>{},Vt=()=>{},oe=Jn(dn,{navigable:!0,labeling:!0,mode:"tune",yFit:"data",onHover:t=>{if(!t||!t.point||!t.point.scored){Ft.textContent="";return}let a=t.point;Ft.textContent=`t=${Rn(a.timestamp)} value=${rn(a.value)} band=[${rn(a.lower)}, ${rn(a.upper)}]`+(a.isAnomaly?` \u26A0 ${a.direction} (sev ${a.severity.toFixed(2)})`:"")},onIncidentsChange:()=>{fe(),vt()},onThresholdChange:t=>un(t),onLassoChange:t=>Vt(t),onAlertReviewChange:(t,a)=>{let p=I.indexOf(t),M=p>=0?O[p]:[t,t];l(M[0],M[1],a==="unreviewed"?null:a),st()}});oe.setIncidents(y),z&&oe.setCaptureWindow(z);let st=()=>{re=D(),ft&&oe.render({series:L,scored:xe,params:ft,alerts:re,incidents:y}),vt(),fe()},xt=m("div","dtk-th"),qt=m("div","dtk-th-toggles"),yt=m("button","dtk-th-toggle","Threshold capture");yt.type="button",yt.title="Mark incidents fast: set a horizontal line and grab every contiguous span above (or below) it. Click the chart to set the line, drag across it to limit the capture to a time window.",qt.appendChild(yt);let wt=m("button","dtk-th-toggle","Lasso anomalies");wt.type="button",wt.title="Draw a freeform loop around a cloud of anomaly dots on the chart below \u2014 each run of consecutive anomalies (small gaps bridged) becomes one incident span. The ideal way to turn what the detector already flags into ground-truth incidents.",qt.appendChild(wt),xt.appendChild(qt);let We=m("div","dtk-th-bar"),lt=m("select","dtk-th-sel");for(let[t,a]of[["above","above the line"],["below","below the line"]]){let p=m("option");p.value=t,p.textContent=a,lt.appendChild(p)}let Ze=m("input","dtk-th-num");Ze.type="number",Ze.step="any",Ze.placeholder="hover chart";let ze=m("input","dtk-th-num");ze.type="number",ze.min="0",ze.step="1",ze.value="0";let Ut=m("span","dtk-th-scope"),at=m("button","dtk-inc-btn","\u21BA whole view");at.type="button",at.style.display="none";let dt=m("button","dtk-apply-btn dtk-th-add","Add 0 spans");dt.type="button",dt.disabled=!0;let W=m("button","dtk-inc-btn","Done");W.type="button";let Be=(t,a)=>{let p=m("label","dtk-th-grp");return p.appendChild(m("span","dtk-th-lbl",t)),p.appendChild(a),p};We.appendChild(Be("grab points",lt)),We.appendChild(Be("line value",Ze)),We.appendChild(Be("bridge gaps \u2264",ze)),We.appendChild(Ut),We.appendChild(at),We.appendChild(dt),We.appendChild(W),We.style.display="none",xt.appendChild(We);let Ae=m("div","dtk-th-bar"),De=m("span","dtk-th-scope","draw a loop around the anomaly dots on the chart below"),ae=m("button","dtk-inc-btn","Done");ae.type="button",Ae.appendChild(De),Ae.appendChild(ae),Ae.style.display="none",xt.appendChild(Ae),le.appendChild(xt);let Ue=m("div","dtk-th-bar dtk-tune-reviewbar");Ue.appendChild(m("span","dtk-th-scope","Click a red alert marker to confirm it valid (green) or mark it a false alarm \u2014 cycle un-reviewed \u2192 valid \u2192 false."));let Gt=m("button","dtk-apply-btn dtk-th-add","Confirm all unreviewed valid");Gt.type="button",Gt.onclick=()=>{for(let[t,a]of O)te(t,a)||l(t,a,"valid");st()};let _t=m("button","dtk-inc-btn","Clear verdicts");_t.type="button",_t.onclick=()=>{ie.length=0,st()},Ue.appendChild(Gt),Ue.appendChild(_t),Ke.appendChild(Ue);let mn={tune:"Tune \xB7 controls",review:"Review \xB7 verdicts",label:"Label \xB7 incidents"};function Yt(t){oe.setMode(t),Object.keys(Bt).forEach(a=>{var p;return(p=Bt[a])==null?void 0:p.classList.toggle("on",a===t)}),t!=="label"&&(ct(!1),ut(!1)),K.style.display=t==="tune"?"":"none",Ke.style.display=t==="review"?"":"none",le.style.display=t==="label"?"":"none",ot.style.display=t==="tune"?"":"none",Ye.textContent=mn[t]}let kt=!1,Mt=!1,Et=!1,ct=t=>{kt=t,yt.classList.toggle("on",t),We.style.display=t?"flex":"none",oe.setThresholdMode(t),t&&Mt&&ut(!1)},ut=t=>{Mt=t,wt.classList.toggle("on",t),Ae.style.display=t?"flex":"none",oe.setLassoMode(t),t&&kt&&ct(!1)};yt.onclick=()=>ct(!kt),W.onclick=()=>ct(!1),wt.onclick=()=>ut(!Mt),ae.onclick=()=>ut(!1),lt.onchange=()=>oe.setThresholdDirection(lt.value),Ze.oninput=()=>{let t=Ze.value.trim();Et=!0,oe.setThresholdValue(t!==""&&!isNaN(Number(t))?Number(t):null),Et=!1},ze.oninput=()=>oe.setThresholdGap(Number(ze.value)||0),at.onclick=()=>oe.clearCaptureWindow(),dt.onclick=()=>{oe.applyThreshold()},window.addEventListener("keydown",t=>{t.key==="Escape"&&(kt?ct(!1):Mt&&ut(!1))}),Vt=t=>{De.textContent=t.active?`${t.anomalies} anomal${t.anomalies===1?"y":"ies"} \u2192 ${t.incidents} incident${t.incidents===1?"":"s"} \u2014 release to add`:"draw a loop around the anomaly dots on the chart below"},un=t=>{dt.textContent=`Add ${t.runs} span${t.runs===1?"":"s"}`,dt.disabled=t.runs===0,t.locked&&t.value!=null&&!Et&&(Ze.value=String(Math.round(t.value*1e3)/1e3)),at.style.display=t.committed?"":"none",Ut.textContent=t.window?`scope: ${sn(t.windowMs)} painted`:"scope: current view \u2014 drag the chart to limit it"};let mt=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function V(e,n,t){let o=un(e),i=o.map(m=>e[m]),r=o.map(m=>n[m]),s=C(r),c=new Array(r.length),u=0;for(let m=0;m<r.length;m++)u+=r[m],c[m]=(u-.5*r[m])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return V(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(f=>f>o),r=i.map(f=>!f),s=0,c=0;for(let f=0;f<n.length;f++)i[f]?s++:c++;if(s<2||c<2)return 0;let u=(f,M)=>M.filter((x,w)=>f[w]),d=u(r,e),m=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),a=u(i,t),A=P(d,h),p=P(m,a),S=P(N,h),g=P(y,a);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=V(n,t,25),i=V(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function j(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,m,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(m=n.upperBound)!=null?m:null,u=[];for(let y=0;y<i;y++){let h=o[y],a=r[y],A=t[y];if(Number.isNaN(a)){u.push(j(y,A,h,a,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&a<s&&(p=!0,S="below",g=s-a),c!==null&&a>c&&(p=!0,S="above",g=a-c);let f=0;if(p)if(s!==null&&c!==null){let T=c-s;f=T>0?g/T:1/0}else f=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:a,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:f,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),m=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let a=0;a<c;a++){let A=s[a],p=d[a],S=r[a];if(Number.isNaN(p)){h.push(j(a,S,A,p,"missing_data"));continue}let g=Math.max(0,a-n.windowSize),f=a-g,M=d.slice(g,a),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(j(a,S,A,p,"insufficient_data"));continue}let T=new Array(f);for(let l=0;l<f;l++)T[l]=f-l;let z=[];for(let l=0;l<f;l++)x[l]&&z.push(T[l]);let B=Q(z,y),W=0;n.detrend==="linear"&&(W=Sn(w,z,B));let nn=W!==0?w.map((l,O)=>l+W*z[O]):w,G=U(t,nn,B),I={...G};if(m&&N){let l=N[a];for(let O of n.seasonalityComponents){let K=new Array(f).fill(!0);for(let b=0;b<f;b++){let q=N[g+b],v=!0;for(let E of O)if(!q||q[E]!==(l==null?void 0:l[E])){v=!1;break}K[b]=v}let L=[],F=[];for(let b=0;b<f;b++)x[b]&&K[b]&&(L.push(M[b]),F.push(T[b]));if(L.length<n.minSamplesPerGroup)continue;let tn=Q(F,y),on=W!==0?L.map((b,q)=>b+W*F[q]):L,rn=U(t,on,tn);for(let[b,q]of i){let v=G[b],sn=(q==="spread"?v>0:v!==0)?rn[b]/v:1;I[b]*=sn}}}let[D,k]=Nn(t,I,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,R=null,J=0;if(H){let l;p<D?(R="below",l=D-p):(R="above",l=p-k),J=An(t,I,l)}let en=Mn(t,I);h.push({index:a,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:R,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var _=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function Tn(e,n,t){let o=[],i=e.length,r=0;for(;r<i;){if(!(e[r].scored&&e[r].isAnomaly)){r++;continue}let s=r;for(;s+1<i&&e[s+1].scored&&e[s+1].isAnomaly&&e[s+1].timestamp-e[s].timestamp===n;)s++;s-r+1>=t&&o.push({fire:r+t-1,startTs:e[r].timestamp,endTs:e[s].timestamp}),r=s+1}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){_=n.series;return}if(n.type==="run"&&_){let t=n.params,o=Z(_,t);xn(o,t.direction);let i=_.intervalSeconds*1e3,r=Tn(o,i,t.consecutiveAnomalies),s=r.map(m=>m.fire),c=r.map(m=>[m.startTs,m.endTs]),u=$(_,t),d=0;for(let m of o)m.scored&&m.isAnomaly&&d++;self.postMessage({type:"result",id:n.id,scored:o,fires:s,fireSpans:c,eff:u,flagged:d})}};})();
|
|
2
|
+
`],{type:"text/javascript"})));mt.postMessage({type:"series",series:L});let Kt=0,ft=null;mt.onmessage=t=>{let a=t.data;if(a.type!=="result"||a.id!==Kt||!ft)return;Oe.classList.remove("on");let p=ft;xe=a.scored,I=a.fires.map(M=>L.timestamps[M]),O=a.fireSpans,re=D(),oe.render({series:L,scored:a.scored,params:p,alerts:re,incidents:y}),vt(),Cn.textContent=`${a.flagged} flagged \xB7 ${a.fires.length} alert${a.fires.length===1?"":"s"} \xB7 warm-up ${a.eff} pts`,Nt(p),An.textContent=fo(p,Pe)},mt.onerror=()=>{Oe.classList.remove("on"),Cn.textContent="recompute failed \u2014 see the browser console"};let fn=()=>{ft=ke(),Kt+=1,Oe.classList.add("on"),mt.postMessage({type:"run",id:Kt,params:ft})},pn=t=>t>1?sn(w.timestamps[e-1]-w.timestamps[e-t]):"\u2014",hn=t=>{Xe.textContent=t>=e?`${e} pts \xB7 full (${pn(e)})`:`${t} pts \xB7 ${pn(t)}`};hn(e);function Mn(t){L=U(t),mt.postMessage({type:"series",series:L}),ge()}let Xt=0;Te.oninput=()=>{let t=Number(Te.value);hn(t),Xt&&window.clearTimeout(Xt),Xt=window.setTimeout(()=>Mn(t),200)};let Jt=0,ge=()=>{Jt&&window.clearTimeout(Jt),Jt=window.setTimeout(fn,130)},$t=on("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],N.type,t=>{var a;vn((a=co[t])!=null?a:3),Sn(),ge()},"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).");K.appendChild($t.row);let Zt=It("Lower bound",{min:Ne,max:He,step:be,value:we,fmt:t=>rn(t),hint:"Manual bounds: values below this read as anomalous. Drag in from the data range to see how many points fall outside (and how many alerts that yields)."},ge);K.appendChild(Zt.row);let Qt=It("Upper bound",{min:Ne,max:He,step:be,value:_e,fmt:t=>rn(t),hint:"Manual bounds: values above this read as anomalous."},ge);K.appendChild(Qt.row);let St=It("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:N.threshold,fmt:t=>t.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},ge);K.appendChild(St.row);let bn=St.row.querySelector("input"),gn=St.row.querySelector(".dtk-ctl-val"),vn=t=>{bn&&(bn.value=String(t)),gn&&(gn.textContent=t.toFixed(1))},o=Math.max(50,Math.min(2e3,Math.floor(e/2))),r=Math.max(o,N.windowSize),d=It("Window size (points)",{min:Math.max(1,Math.min(10,N.windowSize)),max:r,step:1,value:N.windowSize,fmt:t=>`${t} \xB7 ${sn(t*n.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},ge);K.appendChild(d.row);let i=on("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],N.windowWeights,()=>{Sn(),ge()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");K.appendChild(i.row);let s=It("Half-life (points)",{min:1,max:r,step:1,value:(Bn=N.halfLife)!=null?Bn:Math.max(5,Math.round(N.windowSize/20)),fmt:t=>`${t} \xB7 ${sn(t*n.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},ge),u=s.row;u.style.display=N.windowWeights==="exponential"?"":"none",K.appendChild(u);let f=on("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],N.detrend,ge,"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.");K.appendChild(f.row);let x=on("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],N.smoothing,ge,"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.");K.appendChild(x.row);let A=null;if(n.seasonality_columns.length){let t=n.seasonality_columns,a=m("div","dtk-ctl");A=a,a.appendChild(ln("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 p=Math.min(t.length,6),M=[{label:"\u2014",value:"0"}];for(let g=1;g<=p;g++)M.push({label:`G${g}`,value:String(g)});t.forEach(g=>{let B=m("div","dtk-season-row");B.appendChild(m("span","dtk-season-col",g));let Q=m("div","dtk-seg dtk-season-seg"),ht=[],et=()=>{var pe;return(pe=j.get(g))!=null?pe:0},tn=()=>ht.forEach(pe=>pe.classList.toggle("on",Number(pe.dataset.v)===et()));M.forEach(pe=>{let tt=m("button","dtk-seg-btn",pe.label);tt.type="button",tt.dataset.v=pe.value,tt.title=pe.value==="0"?`ignore ${g}`:`put ${g} in group ${pe.value}`,tt.onclick=()=>{j.set(g,Number(pe.value)),tn(),ge()},ht.push(tt),Q.appendChild(tt)}),tn(),B.appendChild(Q),a.appendChild(B)}),K.appendChild(a)}let S=on("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(Fn=n.direction)!=null?Fn:"any",ge,"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.");Lt.appendChild(S.row);let P=It("Alert: consecutive anomalies",{min:1,max:10,step:1,value:Pe,fmt:t=>String(t),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},t=>{Pe=t,ge()});Lt.appendChild(P.row);let F=m("div","dtk-ctl"),Y=m("label","dtk-check"),ue=m("input");ue.type="checkbox",ue.onchange=()=>{oe.setZeroLine(ue.checked)},Y.title="Draw a horizontal line at y = 0 and include zero in the scale \u2014 for real-valued metrics best read relative to zero.",Y.appendChild(ue),Y.appendChild(document.createTextNode(" Show y = 0 line")),F.appendChild(Y),Lt.appendChild(F);let X=m("div","dtk-ctl dtk-incidents");X.appendChild(ln("Marked incidents","The real incidents you marked (in Label mode). Edit a label, focus to zoom the chart to it, or remove it. Save the set below to incidents/<metric>/ \u2014 the same store dtk autotune reads."));let k=m("div","dtk-inc-list");X.appendChild(k),le.appendChild(X);function E(t){let a=Math.max((t.end-t.start)*.5,n.interval_seconds*1e3*5);oe.setViewWindow(t.start-a,t.end+a)}function R(t){let a=y.indexOf(t);a>=0&&y.splice(a,1),oe.setIncidents(y),fe(),vt()}function se(t){l(t.start,t.end,null),oe.setIncidents(y),st()}function fe(){k.innerHTML="";let t=[...y.map(a=>({iv:a,kind:"manual"})),...Z().map(a=>({iv:a,kind:"alert"}))].sort((a,p)=>a.iv.start-p.iv.start);if(!t.length){k.appendChild(m("div","dtk-inc-empty","None yet \u2014 switch to Label mode and drag across the chart, or confirm alerts in Review mode."));return}for(let{iv:a,kind:p}of t){let M=m("div","dtk-inc-row"+(p==="alert"?" dtk-inc-fromalert":""));if(M.appendChild(m("span","dtk-inc-span",`${Rn(a.start)} \u2192 ${Rn(a.end)}`)),M.appendChild(m("span","dtk-inc-dur",sn(Math.max(0,a.end-a.start)))),p==="alert"){let Q=m("span","dtk-inc-badge","\u2713 confirmed alert");Q.title="A fired alert you confirmed valid (in Review mode). It counts as a real incident \u2014 remove it here to un-confirm the alert.",M.appendChild(Q)}else{let Q=m("input","dtk-inc-label");Q.type="text",Q.value=a.label||"",Q.placeholder="label (optional)",Q.oninput=()=>{a.label=Q.value},M.appendChild(Q)}let g=m("button","dtk-inc-btn","focus");g.type="button",g.onclick=()=>E(a),M.appendChild(g);let B=m("button","dtk-inc-btn dtk-inc-del","\u2715");B.type="button",B.title=p==="alert"?"un-confirm this alert":"remove this incident",B.onclick=()=>p==="alert"?se(a):R(a),M.appendChild(B),k.appendChild(M)}}let pt=[St.row,d.row,i.row,f.row,x.row];A&&pt.push(A);function Sn(){let t=$t.get()==="manual_bounds";for(let a of pt)a.style.display=t?"none":"";Zt.row.style.display=t?"":"none",Qt.row.style.display=t?"":"none",u.style.display=!t&&i.get()==="exponential"?"":"none"}Sn();let Cn=m("div","dtk-tune-stat");Re.appendChild(Cn);let Tn=m("div","dtk-tune-cfg"),en=m("button","dtk-tune-cfg-k");en.type="button",en.title="Show or hide the exact config that will be written on Apply.";let An=m("code","dtk-tune-cfg-v"),Wn=!1,zn=t=>{Wn=t,An.style.display=t?"":"none",en.textContent=`${t?"\u25BE":"\u25B8"} // effective config`};if(en.onclick=()=>zn(!Wn),Tn.appendChild(en),Tn.appendChild(An),zn(!1),ot.appendChild(Tn),n.save_url){let t=m("div","dtk-tune-apply"),a=m("button","dtk-apply-btn","Apply to metric");a.type="button",a.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 p=m("span","dtk-apply-msg");a.onclick=()=>{let M=ke();a.disabled=!0,p.className="dtk-apply-msg info",p.textContent="Applying\u2026",fetch(n.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:M.type,params:Qn(M)},consecutive_anomalies:Pe})}).then(g=>g.ok?g.json():g.text().then(B=>{throw new Error(B||`HTTP ${g.status}`)})).then(g=>{var B;p.className="dtk-apply-msg ok",p.textContent=`Applied \u2192 ${(B=g.saved)!=null?B:"metric"} (previous archived). You can close this tab.`}).catch(g=>{a.disabled=!1,p.className="dtk-apply-msg err",p.textContent=`Apply failed: ${g.message}`})},t.appendChild(a),t.appendChild(p),ot.appendChild(t)}else ot.appendChild(m("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));let eo=t=>'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',Rt=t=>new Date(t).toISOString().slice(0,19).replace("T"," "),to=()=>{let t=[`metric: ${n.metric}`,"timezone: UTC"],a=new Set,p=me().sort((g,B)=>g.start-B.start).filter(g=>{let B=`${Math.round(g.start)}:${Math.round(g.end)}`;return a.has(B)?!1:(a.add(B),!0)});if(!p.length)t.push("incidents: []");else{t.push("incidents:");for(let g of p){let B=g.label?`, label: ${eo(g.label)}`:"";t.push(` - {start: "${Rt(g.start)}", end: "${Rt(g.end)}"${B}}`)}}let M=oe.getCaptureWindow();if(M&&(t.push("capture_windows:"),t.push(` - {start: "${Rt(M.start)}", end: "${Rt(M.end)}"}`)),ie.length){t.push("alert_reviews:");for(let g of[...ie].sort((B,Q)=>B.start-Q.start))t.push(` - {start: "${Rt(g.start)}", end: "${Rt(g.end)}", verdict: ${g.verdict}}`)}return t.join(`
|
|
3
|
+
`)+`
|
|
4
|
+
`},xn=m("div","dtk-tune-apply"),yn=m("input","dtk-setname");yn.type="text",yn.placeholder="name this set (optional)";let wn=m("button","dtk-apply-btn dtk-labels-btn",n.labels_save_url?"Save incidents":"Download incidents");wn.type="button",wn.title="Write the marked incidents to incidents/<metric>/ (versioned) so they also feed the next `dtk autotune`. Saving labels does not end tuning \u2014 keep adjusting and apply the detector when ready.";let Qe=m("span","dtk-apply-msg");wn.onclick=()=>{let t=to();if(!n.labels_save_url){let a=new Blob([t],{type:"text/yaml"}),p=m("a");p.href=URL.createObjectURL(a),p.download=`${n.metric}.yml`,p.click(),URL.revokeObjectURL(p.href),Qe.className="dtk-apply-msg ok",Qe.textContent=`Downloaded \u2014 drop it into incidents/${n.metric}/`;return}Qe.className="dtk-apply-msg info",Qe.textContent="Saving\u2026",fetch(n.labels_save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:yn.value,yaml:t})}).then(a=>a.ok?a.json():a.text().then(p=>{throw new Error(p||`HTTP ${a.status}`)})).then(a=>{var p;Qe.className="dtk-apply-msg ok",Qe.textContent=`Saved \u2192 ${(p=a.saved)!=null?p:"incidents"} (keep tuning, or Apply the detector)`}).catch(a=>{Qe.className="dtk-apply-msg err",Qe.textContent=`Save failed: ${a.message}`})},xn.appendChild(yn),xn.appendChild(wn),xn.appendChild(Qe),le.appendChild(xn),Yt("tune"),fe(),fn(),vt();let Dn=0,Hn=()=>{Dn&&cancelAnimationFrame(Dn),Dn=requestAnimationFrame(()=>oe.resize())};typeof ResizeObserver!="undefined"?new ResizeObserver(Hn).observe(qe):window.addEventListener("resize",Hn)}function rn(n){if(!Number.isFinite(n))return"\u2014";let c=Math.abs(n);return c!==0&&(c<.01||c>=1e6)?n.toExponential(2):Number(n.toFixed(c<1?4:2)).toString()}function Rn(n){return new Date(n).toISOString().slice(0,16).replace("T"," ")}function sn(n){let c=Math.round(n/6e4);if(c<60)return`${c}m`;let b=Math.floor(c/60);if(b<24)return`${b}h`;let e=Math.floor(b/24),w=b%24;return w?`${e}d ${w}h`:`${e}d`}function ho(n){return n%86400===0?`${n/86400}d`:n%3600===0?`${n/3600}h`:n%60===0?`${n/60}min`:`${n}s`}var Zn=!1;function bo(){if(Zn)return;Zn=!0;let n=`
|
|
5
|
+
.dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
|
|
6
|
+
--paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
|
|
7
|
+
--mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
|
|
8
|
+
--sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
|
|
9
|
+
.dtk-tune-root{max-width:1680px;margin:0 auto;padding:12px 16px;font-family:var(--sans);color:var(--ink);
|
|
10
|
+
height:100dvh;display:flex;flex-direction:column;gap:10px;overflow:hidden;}
|
|
11
|
+
.dtk-tune-header{flex:0 0 auto;}
|
|
12
|
+
.dtk-tune-titlerow{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
|
|
13
|
+
.dtk-tune-title{font-size:19px;margin:0;font-weight:700;}
|
|
14
|
+
.dtk-tune-badge{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.06em;
|
|
15
|
+
color:#fff;background:var(--c);border-radius:999px;padding:3px 9px;}
|
|
16
|
+
.dtk-tune-sub{color:var(--muted);font-size:12px;margin-top:2px;font-family:var(--mono);}
|
|
17
|
+
.dtk-tune-desc{color:var(--muted);font-size:12px;margin-top:3px;white-space:pre-wrap;max-height:2.6em;overflow:auto;}
|
|
18
|
+
/* cockpit: chart-windshield (stage) + always-visible mode-aware control rail */
|
|
19
|
+
.dtk-tune-cockpit{display:flex;gap:12px;flex:1;min-height:0;}
|
|
20
|
+
.dtk-tune-stage{position:relative;display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}
|
|
21
|
+
.dtk-tune-hud{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;flex:0 0 auto;}
|
|
22
|
+
.dtk-tune-stagefoot{flex:0 0 auto;display:flex;flex-direction:column;gap:6px;}
|
|
23
|
+
.dtk-tune-rail{flex:0 0 340px;display:flex;flex-direction:column;min-height:0;background:var(--surface);
|
|
24
|
+
border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
|
25
|
+
.dtk-tune-railhead{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:8px;
|
|
26
|
+
padding:9px 12px;border-bottom:1px solid var(--border);}
|
|
27
|
+
.dtk-rail-title{font-family:var(--mono);font-size:11px;color:var(--muted);text-transform:uppercase;
|
|
28
|
+
letter-spacing:.06em;flex:1 1 auto;}
|
|
29
|
+
.dtk-tune-railfoot{flex:0 0 auto;display:flex;flex-direction:column;gap:8px;padding:11px 12px;
|
|
30
|
+
border-top:1px solid var(--border);background:var(--paper);}
|
|
31
|
+
.dtk-rail-open{position:absolute;top:50%;right:6px;transform:translateY(-50%);z-index:6;
|
|
32
|
+
border:1px solid var(--border);background:var(--surface);color:var(--ink);border-radius:8px;
|
|
33
|
+
padding:13px 7px;font-size:15px;cursor:pointer;box-shadow:0 1px 6px rgba(27,25,22,.14);}
|
|
34
|
+
.dtk-rail-open:hover{border-color:var(--c);color:var(--c7);}
|
|
35
|
+
.dtk-dock-toggle{flex:0 0 auto;border:1px solid var(--border);background:var(--surface);color:var(--muted);
|
|
36
|
+
border-radius:7px;padding:4px 10px;font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;line-height:1;}
|
|
37
|
+
.dtk-dock-toggle:hover{border-color:var(--c);color:var(--c7);}
|
|
38
|
+
.dtk-tune-controls{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:14px;padding:14px;}
|
|
39
|
+
.dtk-rail-group{display:flex;flex-direction:column;gap:14px;}
|
|
40
|
+
.dtk-ctl{display:flex;flex-direction:column;gap:6px;}
|
|
41
|
+
.dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
|
|
42
|
+
.dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
|
|
43
|
+
.dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
|
|
44
|
+
.dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
|
|
45
|
+
.dtk-seg.dtk-wrap{flex-wrap:wrap;}
|
|
46
|
+
.dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
|
|
47
|
+
font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
|
|
48
|
+
.dtk-seg-btn:hover{color:var(--ink);}
|
|
49
|
+
.dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
|
|
50
|
+
.dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
|
|
51
|
+
.dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
|
|
52
|
+
.dtk-tune-chart{position:relative;width:100%;flex:1;min-height:220px;background:var(--surface);
|
|
53
|
+
border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
|
54
|
+
.dtk-tune-chart canvas{width:100%;height:100%;display:block;}
|
|
55
|
+
.dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
|
|
56
|
+
.dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
|
|
57
|
+
.dtk-tune-warn{font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--c7);
|
|
58
|
+
background:rgba(240,173,78,0.13);border:1px solid rgba(240,173,78,0.5);border-radius:8px;padding:8px 11px;}
|
|
59
|
+
.dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:8px 11px;font-family:var(--mono);
|
|
60
|
+
font-size:12px;overflow-x:auto;}
|
|
61
|
+
.dtk-tune-cfg-k{display:flex;width:100%;border:0;background:transparent;color:var(--faint);
|
|
62
|
+
font-family:var(--mono);font-size:11.5px;cursor:pointer;padding:0;text-align:left;}
|
|
63
|
+
.dtk-tune-cfg-k:hover{color:#e6e0d4;}
|
|
64
|
+
.dtk-tune-cfg-v{display:block;color:#e6e0d4;white-space:pre-wrap;word-break:break-word;margin-top:6px;}
|
|
65
|
+
.dtk-tune-apply{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
|
66
|
+
.dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
|
|
67
|
+
font-size:14px;font-weight:600;cursor:pointer;}
|
|
68
|
+
.dtk-apply-btn:hover{background:var(--c7);}
|
|
69
|
+
.dtk-apply-btn:disabled{opacity:.55;cursor:default;}
|
|
70
|
+
.dtk-apply-msg{font-size:13px;}
|
|
71
|
+
.dtk-apply-msg.ok{color:var(--green);}
|
|
72
|
+
.dtk-apply-msg.err{color:var(--anom);}
|
|
73
|
+
.dtk-apply-msg.info{color:var(--muted);}
|
|
74
|
+
.dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
|
|
75
|
+
border-radius:8px;padding:10px 12px;}
|
|
76
|
+
.dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
|
|
77
|
+
.dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
|
|
78
|
+
border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
|
|
79
|
+
.dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
|
|
80
|
+
.dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
|
|
81
|
+
.dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
|
|
82
|
+
background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
|
|
83
|
+
padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
|
|
84
|
+
.dtk-tune-spin.on{display:inline-flex;}
|
|
85
|
+
.dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
|
|
86
|
+
border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
|
|
87
|
+
@keyframes dtk-spin{to{transform:rotate(360deg);}}
|
|
88
|
+
.dtk-tune-legend{flex:0 0 auto;display:flex;align-items:center;flex-wrap:wrap;gap:8px 16px;font-size:12px;
|
|
89
|
+
color:var(--muted);padding:6px 12px;background:var(--surface);border:1px solid var(--border);border-radius:9px;}
|
|
90
|
+
.dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
|
|
91
|
+
.dtk-leg-sw{display:inline-block;flex:0 0 auto;}
|
|
92
|
+
.dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
|
|
93
|
+
.dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
|
|
94
|
+
border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
|
|
95
|
+
.dtk-leg-sw.center{width:16px;height:2px;
|
|
96
|
+
background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
|
|
97
|
+
.dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
|
|
98
|
+
.dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
|
|
99
|
+
border-top:7px solid var(--anom);}
|
|
100
|
+
.dtk-leg-sw.alert-ok{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
|
|
101
|
+
border-top:7px solid var(--green);}
|
|
102
|
+
.dtk-leg-sw.alert-no{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
|
|
103
|
+
border-top:7px solid #5a7a8c;}
|
|
104
|
+
.dtk-leg-txt{white-space:nowrap;}
|
|
105
|
+
.dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
106
|
+
.dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
|
|
107
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
108
|
+
.dtk-season-seg{flex:0 0 auto;padding:2px;}
|
|
109
|
+
.dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
|
|
110
|
+
.dtk-tune-metrics{display:flex;flex-wrap:wrap;gap:8px;margin:0;flex:0 1 auto;}
|
|
111
|
+
.dtk-m-chip{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;background:var(--surface);
|
|
112
|
+
border:1px solid var(--border);border-radius:10px;font-size:13px;}
|
|
113
|
+
.dtk-m-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;}
|
|
114
|
+
.dtk-m-v{font-family:var(--mono);font-weight:700;font-size:15px;color:var(--ink);}
|
|
115
|
+
.dtk-m-l{color:var(--faint);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
|
|
116
|
+
.dtk-m-sub{color:var(--muted);font-family:var(--mono);font-size:11.5px;}
|
|
117
|
+
.dtk-m-chip.over{border-color:var(--anom);box-shadow:inset 0 0 0 1px rgba(214,50,50,.5);}
|
|
118
|
+
.dtk-m-chip.over .dtk-m-sub{color:var(--anom);font-weight:600;}
|
|
119
|
+
.dtk-tune-modes{display:inline-flex;gap:4px;background:var(--ink);border-radius:9px;padding:4px;margin:0;flex:0 0 auto;}
|
|
120
|
+
.dtk-mode-btn{border:0;background:transparent;color:#c9c2b4;font-family:var(--sans);font-size:13px;font-weight:600;
|
|
121
|
+
padding:7px 16px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}
|
|
122
|
+
.dtk-mode-btn:hover{color:#fff;}
|
|
123
|
+
.dtk-mode-btn.on{background:var(--c);color:#fff;}
|
|
124
|
+
.dtk-tune-reviewbar{align-items:center;}
|
|
125
|
+
.dtk-tune-reviewbar .dtk-apply-btn{background:var(--green);}
|
|
126
|
+
.dtk-tune-reviewbar .dtk-apply-btn:hover{background:#27815d;}
|
|
127
|
+
.dtk-th{display:flex;flex-direction:column;gap:8px;margin:2px 0 6px;}
|
|
128
|
+
.dtk-th-toggles{display:flex;gap:8px;flex-wrap:wrap;}
|
|
129
|
+
.dtk-th-toggle{align-self:flex-start;border:1px solid var(--border);background:var(--surface);
|
|
130
|
+
color:var(--muted);border-radius:8px;padding:6px 12px;font-family:var(--sans);font-size:12.5px;cursor:pointer;}
|
|
131
|
+
.dtk-th-toggle:hover{border-color:var(--c);color:var(--c7);}
|
|
132
|
+
.dtk-th-toggle.on{background:var(--c);border-color:var(--c);color:#fff;}
|
|
133
|
+
.dtk-th-bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 14px;padding:11px 13px;
|
|
134
|
+
background:var(--surface);border:1px solid var(--border);border-radius:10px;}
|
|
135
|
+
.dtk-th-grp{display:flex;flex-direction:column;gap:3px;}
|
|
136
|
+
.dtk-th-lbl{font-family:var(--mono);font-size:10.5px;color:var(--faint);text-transform:uppercase;letter-spacing:.05em;}
|
|
137
|
+
.dtk-th-num,.dtk-th-sel{background:var(--paper);color:var(--ink);border:1px solid var(--border);
|
|
138
|
+
border-radius:6px;padding:5px 8px;font-family:var(--mono);font-size:12px;}
|
|
139
|
+
.dtk-th-num{width:96px;}
|
|
140
|
+
.dtk-th-num:focus,.dtk-th-sel:focus{outline:none;border-color:var(--c);}
|
|
141
|
+
.dtk-th-scope{font-family:var(--mono);font-size:11px;color:var(--muted);align-self:center;flex:1 1 160px;}
|
|
142
|
+
.dtk-th-add{padding:7px 14px;}
|
|
143
|
+
.dtk-th-add:disabled{opacity:.5;cursor:default;}
|
|
144
|
+
.dtk-incidents{gap:8px;}
|
|
145
|
+
.dtk-inc-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow:auto;}
|
|
146
|
+
.dtk-inc-empty{font-size:12px;color:var(--faint);font-style:italic;}
|
|
147
|
+
.dtk-inc-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:var(--paper);
|
|
148
|
+
border:1px solid var(--border);border-radius:7px;padding:6px 8px;}
|
|
149
|
+
.dtk-inc-span{font-family:var(--mono);font-size:10.5px;color:var(--ink);}
|
|
150
|
+
.dtk-inc-dur{font-family:var(--mono);font-size:10.5px;color:var(--muted);}
|
|
151
|
+
.dtk-inc-label{flex:1 1 90px;min-width:70px;background:var(--surface);color:var(--ink);
|
|
152
|
+
border:1px solid var(--border);border-radius:5px;padding:4px 7px;font-family:var(--sans);font-size:11.5px;}
|
|
153
|
+
.dtk-inc-label:focus{outline:none;border-color:var(--c);}
|
|
154
|
+
.dtk-inc-btn{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;
|
|
155
|
+
padding:3px 8px;font-size:11px;cursor:pointer;font-family:var(--sans);}
|
|
156
|
+
.dtk-inc-btn:hover{border-color:var(--c);color:var(--c7);}
|
|
157
|
+
.dtk-inc-del{color:var(--anom);}
|
|
158
|
+
.dtk-inc-fromalert{border-color:rgba(46,158,115,.5);background:rgba(46,158,115,.08);}
|
|
159
|
+
.dtk-inc-badge{flex:1 1 90px;min-width:70px;font-family:var(--mono);font-size:10.5px;color:var(--green);
|
|
160
|
+
display:inline-flex;align-items:center;font-weight:600;}
|
|
161
|
+
.dtk-setname{background:var(--surface);color:var(--ink);border:1px solid var(--border);border-radius:8px;
|
|
162
|
+
padding:9px 11px;font-family:var(--sans);font-size:13px;min-width:180px;}
|
|
163
|
+
.dtk-setname::placeholder{color:var(--faint);}
|
|
164
|
+
.dtk-setname:focus{outline:none;border-color:var(--c);}
|
|
165
|
+
.dtk-labels-btn{background:var(--surface);color:var(--ink);border:1px solid var(--border);}
|
|
166
|
+
.dtk-labels-btn:hover{background:var(--paper);border-color:var(--c);color:var(--c7);}
|
|
167
|
+
/* Narrow viewports: drop the cockpit to a scrolling stack (chart over rail). */
|
|
168
|
+
@media (max-width:900px){
|
|
169
|
+
.dtk-tune-root{height:auto;overflow:visible;}
|
|
170
|
+
.dtk-tune-cockpit{flex-direction:column;}
|
|
171
|
+
.dtk-tune-rail{flex:0 0 auto;width:100%;}
|
|
172
|
+
.dtk-tune-controls{overflow:visible;}
|
|
173
|
+
.dtk-tune-chart{flex:0 0 auto;height:54vh;min-height:320px;}
|
|
174
|
+
.dtk-rail-open{display:none!important;}
|
|
175
|
+
}
|
|
176
|
+
`,c=document.createElement("style");c.textContent=n,document.head.appendChild(c)}window.__DTK_TUNE__={render:po};})();
|
|
@@ -60,6 +60,11 @@ _MIN_SAMPLES_PER_GROUP_DEFAULT = {"mad": 10, "zscore": 3, "iqr": 4}
|
|
|
60
60
|
# detectors plus the stateless manual_bounds (lower/upper threshold) detector.
|
|
61
61
|
_TUNABLE_TYPES = ("mad", "zscore", "iqr", "manual_bounds")
|
|
62
62
|
|
|
63
|
+
# Built-in false-alert-rate budget used by the cockpit's quality bar when neither
|
|
64
|
+
# the metric nor the project sets one. Lax on purpose (warn only when more than
|
|
65
|
+
# half the alerts are false) — a per-metric/project `false_alert_budget` tightens it.
|
|
66
|
+
DEFAULT_FALSE_ALERT_BUDGET = 0.5
|
|
67
|
+
|
|
63
68
|
|
|
64
69
|
def _normalize_seasonality_components(
|
|
65
70
|
components: list[str | list[str]] | None,
|
|
@@ -178,6 +183,7 @@ def build_tune_payload(
|
|
|
178
183
|
incidents: list[dict[str, str]] | None = None,
|
|
179
184
|
capture_windows: list[dict[str, str]] | None = None,
|
|
180
185
|
alert_reviews: list[dict[str, str]] | None = None,
|
|
186
|
+
false_alert_budget: float | None = None,
|
|
181
187
|
) -> dict[str, Any]:
|
|
182
188
|
"""Assemble the interactive tuning payload from the persisted ``_dtk_datapoints``.
|
|
183
189
|
|
|
@@ -200,6 +206,7 @@ def build_tune_payload(
|
|
|
200
206
|
seed_incidents = incidents or []
|
|
201
207
|
seed_capture = capture_windows or []
|
|
202
208
|
seed_reviews = alert_reviews or []
|
|
209
|
+
fa_budget = false_alert_budget if false_alert_budget is not None else DEFAULT_FALSE_ALERT_BUDGET
|
|
203
210
|
name = metric_config.name
|
|
204
211
|
interval = metric_config.get_interval()
|
|
205
212
|
interval_seconds = interval.seconds
|
|
@@ -263,6 +270,7 @@ def build_tune_payload(
|
|
|
263
270
|
"incidents": seed_incidents,
|
|
264
271
|
"capture_windows": seed_capture,
|
|
265
272
|
"alert_reviews": seed_reviews,
|
|
273
|
+
"false_alert_budget": fa_budget,
|
|
266
274
|
"labels_save_url": None,
|
|
267
275
|
}
|
|
268
276
|
if start is None or end is None:
|
|
@@ -305,5 +313,6 @@ def build_tune_payload(
|
|
|
305
313
|
"incidents": seed_incidents,
|
|
306
314
|
"capture_windows": seed_capture,
|
|
307
315
|
"alert_reviews": seed_reviews,
|
|
316
|
+
"false_alert_budget": fa_budget,
|
|
308
317
|
"labels_save_url": None,
|
|
309
318
|
}
|