detectkit 0.35.0__tar.gz → 0.36.1__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.35.0/detectkit.egg-info → detectkit-0.36.1}/PKG-INFO +1 -1
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/__init__.py +1 -1
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/cli.md +11 -5
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/tune.py +10 -1
- detectkit-0.36.1/detectkit/tuning/assets/tune.js +131 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/tuning/payload.py +61 -4
- {detectkit-0.35.0 → detectkit-0.36.1/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.35.0/detectkit/tuning/assets/tune.js +0 -117
- {detectkit-0.35.0 → detectkit-0.36.1}/LICENSE +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/MANIFEST.in +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/README.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/html_labeler.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/result.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/_output.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/cli/main.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/config/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/config/profile.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/config/project_config.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/config/validator.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/core/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/core/interval.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/core/models.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/database/tables.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/tuning/html.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/tuning/server.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit/utils/stats.py +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/pyproject.toml +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/requirements.txt +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/setup.cfg +0 -0
- {detectkit-0.35.0 → detectkit-0.36.1}/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.36.1"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -90,14 +90,20 @@ to a single metric.
|
|
|
90
90
|
|
|
91
91
|
**Mark incidents + see alert quality live.** A second, **synced** chart beneath
|
|
92
92
|
the detector view lets you **mark the real incidents** (drag to create a span,
|
|
93
|
-
drag its edges/middle to adjust, ✕ or Delete to remove)
|
|
93
|
+
drag its edges/middle to adjust, ✕ or Delete to remove), or **Threshold capture**
|
|
94
|
+
every contiguous span past a horizontal line in one shot (set the line by click or
|
|
95
|
+
value, above/below, optional gap-bridging, optionally limited to a painted time
|
|
96
|
+
window — the same tool as the autotune labeler; the window is saved as
|
|
97
|
+
`capture_windows` and restored on reopen). As you tune, a metrics
|
|
94
98
|
bar shows two operator numbers: **incident catch rate (recall)** — how many marked
|
|
95
99
|
incidents your config catches — and **false-alert rate** — what share of alerts
|
|
96
|
-
fall outside any real incident ("≈1 in N false")
|
|
100
|
+
fall outside any real incident ("≈1 in N false"); only incidents within the loaded
|
|
101
|
+
window are scored. **Save incidents** writes a
|
|
97
102
|
versioned `incidents/<metric>/*.yml` — the same store `dtk autotune` reads, so the
|
|
98
|
-
same labels feed the next supervised tune (it seeds from the newest file on open
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
same labels feed the next supervised tune (it seeds from the newest file on open,
|
|
104
|
+
**widening the loaded window to cover seeded incidents** so older ones still render
|
|
105
|
+
and count). Saving incidents does not end the session; only **Apply** does. A
|
|
106
|
+
**y = 0 line** toggle (shared with `dtk run --report`) shows the metric relative to zero.
|
|
101
107
|
|
|
102
108
|
Safe write-back: the config is validated before anything is written, the previous
|
|
103
109
|
YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
|
|
@@ -15,7 +15,11 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import click
|
|
17
17
|
|
|
18
|
-
from detectkit.autotune.labels import
|
|
18
|
+
from detectkit.autotune.labels import (
|
|
19
|
+
load_capture_windows,
|
|
20
|
+
load_incidents_for_display,
|
|
21
|
+
newest_labels_file,
|
|
22
|
+
)
|
|
19
23
|
from detectkit.cli._output import echo_done, echo_error, echo_noop
|
|
20
24
|
from detectkit.cli.commands.autotune import _load_project
|
|
21
25
|
from detectkit.cli.commands.run import parse_date, select_metrics
|
|
@@ -65,12 +69,16 @@ def run_tune(
|
|
|
65
69
|
# just yields no seed.
|
|
66
70
|
incidents_dir = project_root / "incidents" / name
|
|
67
71
|
preload_incidents: list[dict[str, str]] = []
|
|
72
|
+
preload_capture: list[dict[str, str]] = []
|
|
68
73
|
newest = newest_labels_file(incidents_dir)
|
|
69
74
|
if newest is not None:
|
|
70
75
|
try:
|
|
71
76
|
preload_incidents = load_incidents_for_display(
|
|
72
77
|
newest, interval_seconds=interval_seconds, metric_name=name
|
|
73
78
|
)
|
|
79
|
+
preload_capture = load_capture_windows(
|
|
80
|
+
newest, interval_seconds=interval_seconds, metric_name=name
|
|
81
|
+
)
|
|
74
82
|
click.echo(
|
|
75
83
|
f" Seeded {len(preload_incidents)} incident(s) from "
|
|
76
84
|
f"{newest.relative_to(project_root)}"
|
|
@@ -90,6 +98,7 @@ def run_tune(
|
|
|
90
98
|
end=to_dt,
|
|
91
99
|
project_name=project_name,
|
|
92
100
|
incidents=preload_incidents,
|
|
101
|
+
capture_windows=preload_capture,
|
|
93
102
|
)
|
|
94
103
|
n_points = len(payload["points"])
|
|
95
104
|
if n_points == 0:
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";(()=>{var dn={"--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(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||dn[t]||"#888"}function cn(t){let a=t.replace("#","").trim();a.length===3&&(a=a[0]+a[0]+a[1]+a[1]+a[2]+a[2]);let h=parseInt(a,16);return a.length!==6||Number.isNaN(h)?[209,91,54]:[h>>16&255,h>>8&255,h&255]}function R(t,a){let[h,e,g]=cn(t);return`rgba(${h},${e},${g},${a})`}function Jt(t){let a=Math.max(1,window.devicePixelRatio||1),h=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(h*a),t.height=Math.round(e*a),a}var qt=Number.isFinite;function Vt(t,a,h,e,g,L,B,b,r,y,x,w){let K=a.length,M=Math.max(1,Math.round(B)),O=g-e||1,Se=0;for(let T=0;T<K;T++){let ee=h[T];!qt(ee)||a[T]<e||a[T]>g||Se++}if(t.strokeStyle=y,t.lineWidth=x*w,t.lineJoin="round",t.beginPath(),Se<=M){let T=!1;for(let ee=0;ee<K;ee++){let X=h[ee],V=a[ee];if(!qt(X)||V<e||V>g){T=!1;continue}let re=b(V),Z=r(X);T?t.lineTo(re,Z):(t.moveTo(re,Z),T=!0)}}else{let T=new Array(M).fill(null),ee=new Array(M).fill(null);for(let V=0;V<K;V++){let re=h[V],Z=a[V];if(!qt(re)||Z<e||Z>g)continue;let J=Math.floor((Z-e)/O*(M-1));J=J<0?0:J>M-1?M-1:J,(T[J]===null||re<T[J])&&(T[J]=re),(ee[J]===null||re>ee[J])&&(ee[J]=re)}let X=!1;for(let V=0;V<M;V++){if(ee[V]===null){X=!1;continue}let re=L+V,Z=r(ee[V]),J=r(T[V]);X?t.lineTo(re,Z):(t.moveTo(re,Z),X=!0),t.lineTo(re,J)}}t.stroke()}function Qt(t){let a=Math.abs(t);return a>=1e3?t.toFixed(0):a>=10?t.toFixed(1):a>=1?t.toFixed(2):t.toFixed(3)}function en(t,a){let h=new Date(t).toISOString();return a<2*864e5?h.slice(5,16).replace("T"," "):h.slice(5,10)}function tn(t,a,h){return{left:a.l*h,top:a.t*h,right:t.width-a.r*h,bottom:t.height-a.b*h}}function nn(t,a,h,e,g,L,B){let b=tn(a,h,e),r=Math.max(b.left,Math.min(g(L),b.right));r<=b.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(b.left,b.top,r-b.left,b.bottom-b.top),t.strokeStyle=R(q("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(r,b.top),t.lineTo(r,b.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=R(q("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(B,r+6*e,b.top+5*e),t.restore())}function on(t,a,h,e,g,L,B){let b=tn(a,h,e),r=5*e;t.save();for(let y=0;y<L.length;y++){let x=L[y],w=g(x.t);if(w<b.left-1||w>b.right+1)continue;let K=B(x.kind);t.strokeStyle=R(K,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(w,b.top),t.lineTo(w,b.bottom),t.stroke(),t.fillStyle=K,t.beginPath(),t.moveTo(w-r,b.top),t.lineTo(w+r,b.top),t.lineTo(w,b.top+r*1.4),t.closePath(),t.fill()}t.restore()}var un={mad:1,zscore:2,iqr:4};function mn(t,a){let h=t.seasonalityData;if(!h||h.length===0)return 0;let e=0;for(let g of a){let L=new Set;for(let B of h)L.add(g.map(b=>{var r;return String((r=B==null?void 0:B[b])!=null?r:"")}).join("|"));e=Math.max(e,L.size)}return e}function rn(t,a){let h=t.timestamps.length;if(a.type==="manual_bounds")return Math.min(a.inputType!=="values"?1:0,h);let e=Math.max(a.minSamples,un[a.type]);if(a.smoothing==="sma"&&(e=Math.max(e,a.smoothingWindow-1)),a.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/a.smoothingAlpha))),a.inputType!=="values"&&(e=Math.max(e,1)),a.seasonalityComponents!==null&&a.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let L=mn(t,a.seasonalityComponents);if(L>0){let B=a.minSamplesPerGroup*L;a.windowSize>=B&&(e=Math.max(e,B))}}return Math.min(e,h)}var D={l:52,r:14,t:14,b:26},jt=46,pn=10,fn=13,xe=Number.isFinite,Xe=1e3,Ze=60*Xe,Je=60*Ze,ge=24*Je,Ut=[Xe,2*Xe,5*Xe,10*Xe,15*Xe,30*Xe,Ze,2*Ze,5*Ze,10*Ze,15*Ze,30*Ze,Je,2*Je,3*Je,6*Je,12*Je,ge,2*ge,3*ge,5*ge,7*ge,14*ge];function sn(t,a,h){let e=Math.max(a-t,1);if(e>h*14*ge){let b=[1,2,3,6,12,24,36,60,120,240],r=b[b.length-1];for(let O of b)if(e/(O*30.44*ge)<=h){r=O;break}let y=new Date(t),x=y.getUTCFullYear(),w=y.getUTCMonth();if(r>=12){let O=r/12;x=Math.floor(x/O)*O,w=0}else w=Math.floor(w/r)*r;let K=[],M=Date.UTC(x,w,1);for(;M<=a;)M>=t&&K.push(M),w+=r,x+=Math.floor(w/12),w%=12,M=Date.UTC(x,w,1);return{ticks:K,step:r*30*ge}}let g=Ut[Ut.length-1];for(let b of Ut)if(e/b<=h){g=b;break}let L;if(g%ge===0){let b=new Date(t);L=Date.UTC(b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate());let r=g/ge,y=Math.round(L/ge);L+=(r-y%r)%r*ge}else L=Math.ceil(t/g)*g;let B=[];for(let b=L;b<=a;b+=g)b>=t&&B.push(b);return{ticks:B,step:g}}function Gt(t,a){let h=new Date(t).toISOString();return a>=320*ge?h.slice(0,4):a>=26*ge?h.slice(0,7):a>=ge?h.slice(5,10):a>=Je?h.slice(5,16).replace("T"," "):h.slice(5,19).replace("T"," ")}function Yt(t,a={}){let h=t.getContext("2d");if(!h)throw new Error("chart: 2D context unavailable");let e=h,g=!!a.navigable,L=g&&a.showNavigator!==!1,B=a.yFit==="data",b=!!a.labeling,r=1,y=null,x=-1,w=0,K=!!a.showZeroLine,M=[],O=null,Se=-1,T=null,ee=!1,X=!1,V="above",re=0,Z=null,J=null,ve=null,me=null,Pe=null,I=0,z=1,pe=0,Ce=1,te=0,ie=1,ft="";function Qe(){r=Jt(t)}let j=()=>L?(pn+jt)*r:0,U=()=>t.width-(D.l+D.r)*r,ye=()=>t.height-(D.t+D.b)*r-j(),Te=()=>g?te:I,Ve=()=>g?ie:z,se=()=>Ve()-Te()||1,Ne=()=>z-I||1,$=n=>D.l*r+(n-Te())/se()*U(),ne=n=>t.height-D.b*r-j()-(n-pe)/(Ce-pe||1)*ye(),Ie=()=>t.height-jt*r,ae=()=>(jt-fn)*r,ze=()=>t.width-(D.l+D.r)*r,le=n=>D.l*r+(n-I)/Ne()*ze(),Ot=n=>Ie()+ae()-(n-pe)/(Ce-pe||1)*ae(),et=(n,i,d)=>{let l=new Array(n.length).fill(!1);if(n.length===0)return l;let s=e.measureText(Gt(n[0],d)).width+16*r,u=-1/0;for(let m=0;m<n.length;m++){let v=i(n[m]);v-u>=s&&(l[m]=!0,u=v)}return l},Q=(n,i,d)=>Math.max(i,Math.min(d,n)),He=()=>{let n=y==null?void 0:y.series,i=n?n.timestamps.length:0,d=i>1?Ne()/(i-1):1e3;return Math.max(d*8,1e3)};function Ae(n,i){var s;let d=i-n,l=He();if(d<l){let u=(n+i)/2;n=u-l/2,i=u+l/2,d=l}d>=Ne()&&(n=I,i=z),n<I&&(i+=I-n,n=I),i>z&&(n-=i-z,i=z),te=Q(n,I,z),ie=Q(i,I,z),ee||(s=a.onViewChange)==null||s.call(a,te,ie),X&&oe(),H()}function ht(n,i){g&&(ee=!0,Ae(n,i),ee=!1)}let De=n=>{let i=(n-D.l*r)/(U()||1);return Te()+Q(i,0,1)*se()},Ee=n=>{let i=(n-D.l*r)/(ze()||1);return I+Q(i,0,1)*Ne()},G=n=>{let i=t.height-D.b*r-j(),d=Q((i-n)/(ye()||1),0,1);return pe+d*(Ce-pe)};function We(n){let i=y==null?void 0:y.series;if(!i||i.timestamps.length===0)return-1;let d=(n-D.l*r)/(U()||1),l=Te()+d*se(),s=i.timestamps,u=0,m=s.length-1;for(;u<m;){let v=u+m>>1;s[v]<l?u=v+1:m=v}return u>0&&l-s[u-1]<s[u]-l&&(u-=1),u}function bt(n,i){let d=n.timestamps;if(I=d[0],z=d[d.length-1],g){let m=`${d.length}:${I}:${z}`;m!==ft?(ft=m,te=I,ie=z):(te=Q(te,I,z),ie=Q(Math.max(ie,te+He()),I,z))}let l=1/0,s=-1/0;for(let m of n.values)xe(m)&&(m<l&&(l=m),m>s&&(s=m));if(!B)for(let m of i)m.scored&&(xe(m.lower)&&m.lower<l&&(l=m.lower),xe(m.upper)&&m.upper>s&&(s=m.upper));(!xe(l)||!xe(s))&&(l=0,s=1),K&&(l>0&&(l=0),s<0&&(s=0)),s<=l&&(s=l+1);let u=(s-l)*.06;pe=l-u,Ce=s+u}function tt(n,i,d){let l=y.series.timestamps;Vt(e,l,n,Te(),Ve(),D.l*r,U(),$,ne,i,d,r)}function _t(n,i){let d=[],l=-1;for(let s=Math.max(0,i);s<n.length;s++){let u=n[s];u.scored&&xe(u.lower)&&xe(u.upper)?l===-1&&(l=s):l!==-1&&(d.push([l,s-1]),l=-1)}return l!==-1&&d.push([l,n.length-1]),d}function gt(){if(w=0,!y||t.width===0||t.height===0)return;let{series:n,scored:i,params:d,alerts:l}=y;if(n.timestamps.length===0){e.fillStyle=q("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let s=q("--clay"),u=q("--st-anomaly"),m=q("--faint"),v=q("--muted");e.fillStyle=q("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*r}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let k=0;k<=4;k++){let C=pe+(Ce-pe)*k/4,S=ne(C);e.strokeStyle=R(m,.1),e.lineWidth=1*r,e.beginPath(),e.moveTo(D.l*r,S),e.lineTo(t.width-D.r*r,S),e.stroke(),e.fillStyle=v,e.textAlign="right",e.fillText(Qt(C),(D.l-8)*r,S)}if(K&&pe<=0&&Ce>=0){let k=ne(0);e.strokeStyle=R(v,.6),e.lineWidth=1.25*r,e.beginPath(),e.moveTo(D.l*r,k),e.lineTo(t.width-D.r*r,k),e.stroke(),e.fillStyle=v,e.textAlign="right",e.fillText("0",(D.l-8)*r,k)}e.textBaseline="top";let W=t.height-D.b*r-j();if(g){let k=sn(Te(),Ve(),7),C=et(k.ticks,$,k.step);for(let S=0;S<k.ticks.length;S++){let N=k.ticks[S],ce=$(N);e.strokeStyle=R(m,.1),e.lineWidth=1*r,e.beginPath(),e.moveTo(ce,D.t*r),e.lineTo(ce,W),e.stroke(),C[S]&&(e.fillStyle=v,e.textAlign=ce<(D.l+24)*r?"left":ce>t.width-(D.r+24)*r?"right":"center",e.fillText(Gt(N,k.step),ce,W+7*r))}}else{let k=se();for(let C=0;C<=5;C++){let S=I+k*C/5,N=$(S);e.fillStyle=v,e.textAlign=C===0?"left":C===5?"right":"center",e.fillText(en(S,k),N,W+7*r)}}e.save(),e.beginPath(),e.rect(D.l*r,D.t*r,U(),ye()),e.clip(),xt(),yt();let P=i.length,_=Math.min(rn(n,d),P),F=_<P?n.timestamps[_]:void 0,de=_t(i,_);e.fillStyle=R(s,.13);for(let[k,C]of de){e.beginPath(),e.moveTo($(i[k].timestamp),ne(i[k].upper));for(let S=k+1;S<=C;S++)e.lineTo($(i[S].timestamp),ne(i[S].upper));for(let S=C;S>=k;S--)e.lineTo($(i[S].timestamp),ne(i[S].lower));e.closePath(),e.fill()}e.strokeStyle=R(s,.4),e.lineWidth=1*r;for(let[k,C]of de)for(let S of["upper","lower"]){e.beginPath();for(let N=k;N<=C;N++){let ce=$(i[N].timestamp),qe=ne(i[N][S]);N===k?e.moveTo(ce,qe):e.lineTo(ce,qe)}e.stroke()}e.strokeStyle=R(m,.55),e.lineWidth=1*r,e.setLineDash([3*r,3*r]);for(let[k,C]of de){e.beginPath();for(let S=k;S<=C;S++){let N=i[S].center;if(!xe(N))continue;let ce=$(i[S].timestamp),qe=ne(N);S===k?e.moveTo(ce,qe):e.lineTo(ce,qe)}e.stroke()}if(e.setLineDash([]),d.smoothing!=="none"){tt(n.values,R(s,.28),1.25);let k=i.map(C=>C.processedValue);tt(k,s,1.6)}else tt(n.values,s,1.5);for(let k=_;k<P;k++){let C=i[k];if(!C.scored||!xe(C.value))continue;let S=$(C.timestamp),N=ne(C.value);C.isAnomaly?(e.fillStyle=R(u,.18),e.beginPath(),e.arc(S,N,6*r,0,Math.PI*2),e.fill(),e.fillStyle=u,e.beginPath(),e.arc(S,N,3*r,0,Math.PI*2),e.fill()):n.truthAnomaly[k]&&(e.strokeStyle=R(v,.7),e.lineWidth=1.25*r,e.beginPath(),e.arc(S,N,3.5*r,0,Math.PI*2),e.stroke())}F!==void 0&&nn(e,t,D,r,$,F,"detection at full power \u2192"),x>=0&&x<i.length&&ke(x,d.windowSize,i,n,m),l&&l.length&&on(e,t,D,r,$,l,k=>k==="anomaly"?q("--st-anomaly"):k==="recovery"?q("--st-recovery"):q("--st-nodata")),wt(),e.restore(),L&&je(n,l,s,m,v)}function je(n,i,d,l,s){let u=Ie(),m=u+ae(),v=D.l*r,W=t.width-D.r*r,P=sn(I,z,5);e.save(),e.beginPath(),e.rect(v,u,ze(),ae()),e.clip(),e.strokeStyle=R(l,.1),e.lineWidth=1*r;for(let C of P.ticks){let S=le(C);e.beginPath(),e.moveTo(S,u),e.lineTo(S,m),e.stroke()}Vt(e,n.timestamps,n.values,I,z,v,ze(),le,Ot,R(d,.7),1.1,r),e.restore();let _=le(te),F=le(ie);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(v,u,_-v,ae()),e.fillRect(F,u,W-F,ae()),M.length){let C=q("--st-anomaly");e.fillStyle=R(C,.28);for(let S of M){let N=le(S.start),ce=Math.max(le(S.end)-N,2*r);e.fillRect(N,u,ce,ae())}}if(i&&i.length){let C=q("--st-anomaly");e.fillStyle=R(C,.85);for(let S of i){let N=le(S.t);e.fillRect(N-1*r,u,2*r,ae())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(_,u,F-_,ae()),e.strokeStyle=d,e.lineWidth=1.5*r,e.strokeRect(_,u+1,F-_,ae()-2),e.fillStyle=d;let de=u+ae()/2-8*r;e.fillRect(_-2*r,de,4*r,16*r),e.fillRect(F-2*r,de,4*r,16*r),e.font=`${10*r}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=s;let k=et(P.ticks,le,P.step);for(let C=0;C<P.ticks.length;C++){let S=P.ticks[C],N=le(S);e.strokeStyle=R(l,.25),e.lineWidth=1*r,e.beginPath(),e.moveTo(N,m),e.lineTo(N,m+3*r),e.stroke(),k[C]&&(e.textAlign=N<(D.l+26)*r?"left":N>t.width-(D.r+26)*r?"right":"center",e.fillText(Gt(S,P.step),N,m+5*r))}}function ke(n,i,d,l,s){let u=l.timestamps,m=Math.max(0,n-i),v=n-1,W=D.t*r,P=ye();if(v>=m){let de=l.intervalSeconds*1e3/se()*U()*.5,k=$(u[m])-de,C=$(u[v])+de;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(k,W,C-k,P),e.strokeStyle=R(s,.5),e.lineWidth=1*r,e.beginPath(),e.moveTo(k,W),e.lineTo(k,W+P),e.moveTo(C,W),e.lineTo(C,W+P),e.stroke()}let _=$(u[n]);e.strokeStyle=R(s,.85),e.lineWidth=1*r,e.setLineDash([2*r,2*r]),e.beginPath(),e.moveTo(_,W),e.lineTo(_,W+P),e.stroke(),e.setLineDash([]);let F=d[n];if(F.scored&&xe(F.lower)&&xe(F.upper)){let de=5*r;e.strokeStyle=R(q("--clay"),.85),e.lineWidth=1.5*r;for(let k of[F.lower,F.upper]){let C=ne(k);e.beginPath(),e.moveTo(_-de,C),e.lineTo(_+de,C),e.stroke()}}if(xe(F.value)){let de=ne(F.value);e.fillStyle=q("--term-bg"),e.beginPath(),e.arc(_,de,4*r,0,Math.PI*2),e.fill(),e.strokeStyle=F.isAnomaly?q("--st-anomaly"):q("--clay"),e.lineWidth=2*r,e.beginPath(),e.arc(_,de,4*r,0,Math.PI*2),e.stroke()}}function nt(n,i,d,l,s){e.beginPath(),e.moveTo(n+s,i),e.arcTo(n+d,i,n+d,i+l,s),e.arcTo(n+d,i+l,n,i+l,s),e.arcTo(n,i+l,n,i,s),e.arcTo(n,i,n+d,i,s),e.closePath()}function _e(n,i){let d=q("--st-anomaly"),l=14*r,s=3*r,u=n-l-s,m=D.t*r+s;e.fillStyle=i?R(d,.95):"rgba(27,25,22,0.82)",e.strokeStyle=R(d,.9),e.lineWidth=1*r,nt(u,m,l,l,3*r),e.fill(),e.stroke(),e.strokeStyle=i?"#fff":d,e.lineWidth=1.5*r;let v=4*r;e.beginPath(),e.moveTo(u+v,m+v),e.lineTo(u+l-v,m+l-v),e.moveTo(u+l-v,m+v),e.lineTo(u+v,m+l-v),e.stroke()}let Me=()=>Z!=null?Z:J,ot=()=>{let n=Pe||ve;return n?[Math.min(n.a,n.b),Math.max(n.a,n.b)]:[Te(),Ve()]};function $e(){let n=[],i=Me();if(i==null||!y)return n;let d=y.series.timestamps,l=y.series.values,[s,u]=ot(),m=-1,v=-1,W=0;for(let P=0;P<d.length;P++){if(d[P]<s||d[P]>u)continue;let _=l[P];xe(_)&&(V==="above"?_>i:_<i)?(m===-1&&(m=d[P]),v=d[P],W=0):m!==-1&&(W++,W>re&&(n.push([m,v]),m=-1,W=0))}return m!==-1&&n.push([m,v]),n}function oe(){if(!a.onThresholdChange)return;let[n,i]=ot(),d=Pe||ve;a.onThresholdChange({value:Me(),locked:Z!=null,runs:$e().length,window:d?{start:Math.min(d.a,d.b),end:Math.max(d.a,d.b)}:null,committed:ve!=null,windowMs:i-n})}function vt(n,i){let d=null;for(let l=M.length-1;l>=0;l--){let s=M[l];n<=s.end&&i>=s.start&&(d===null?(s.start=Math.min(s.start,n),s.end=Math.max(s.end,i),d=s):(d.start=Math.min(d.start,s.start),d.end=Math.max(d.end,s.end),O===s&&(O=d),M.splice(l,1)))}d===null&&M.push({start:n,end:i,label:""})}function xt(){if(!X||Me()==null)return;let i=D.t*r,d=ye(),l=q("--st-nodata");for(let[s,u]of $e()){let m=$(s),v=Math.max($(u)-m,2*r);e.fillStyle=R(l,.22),e.fillRect(m,i,v,d),e.strokeStyle=R(l,.6),e.lineWidth=1*r,e.strokeRect(m,i,v,d)}}function wt(){if(!X)return;let n=D.t*r,i=t.height-D.b*r-j(),d=D.l*r,l=t.width-D.r*r,s=q("--st-nodata"),[u,m]=ot(),v=!!(Pe||ve),W=Q($(u),d,l),P=Q($(m),d,l);v&&(e.fillStyle=R(q("--ink"),.5),e.fillRect(d,n,Math.max(0,W-d),ye()),e.fillRect(P,n,Math.max(0,l-P),ye()),e.strokeStyle=R(s,.7),e.lineWidth=1*r,e.setLineDash([3*r,3*r]),e.beginPath(),e.moveTo(W,n),e.lineTo(W,i),e.moveTo(P,n),e.lineTo(P,i),e.stroke(),e.setLineDash([]));let _=Me();if(_!=null&&_>=pe&&_<=Ce){let F=ne(_);e.strokeStyle=s,e.lineWidth=1.5*r,e.setLineDash([6*r,4*r]),e.beginPath(),e.moveTo(W,F),e.lineTo(P,F),e.stroke(),e.setLineDash([])}}function yt(){let n=D.t*r,i=ye(),d=q("--st-anomaly"),l=D.l*r,s=t.width-D.r*r;for(let u=0;u<M.length;u++){let m=M[u],v=$(m.start),W=$(m.end);if(W<l-1||v>s+1)continue;let P=Math.max(W-v,2*r),_=b&&m===O;e.fillStyle=R(d,_?.3:.16),e.fillRect(v,n,P,i),e.strokeStyle=R(d,_?.95:.5),e.lineWidth=(_?2:1)*r,e.strokeRect(v,n,P,i),b&&(e.fillStyle=R(d,.95),e.fillRect(v-1.5*r,n,3*r,i),e.fillRect(W-1.5*r,n,3*r,i),(W-v>=22*r||_)&&_e(W,_||u===Se))}if(b&&T&&T.mode==="new"){let u=$(T.a),m=$(T.b);e.fillStyle=R(q("--st-nodata"),.28),e.fillRect(Math.min(u,m),n,Math.abs(m-u),i)}}function Ue(){var n;(n=a.onIncidentsChange)==null||n.call(a,M)}function Be(n){let i=M.indexOf(n);i<0||(M.splice(i,1),O===n&&(O=null),Se=-1,Ue(),H())}function rt(n,i){let d=D.t*r,l=6*r;for(let s=0;s<M.length;s++){let u=$(M[s].end);if(u-$(M[s].start)>=22*r||M[s]===O){let m=14*r,v=3*r,W=u-m-v,P=d+v;if(n>=W&&n<=W+m&&i>=P&&i<=P+m)return{i:s,edge:"del"}}}for(let s=0;s<M.length;s++){let u=$(M[s].start),m=$(M[s].end);if(Math.abs(n-u)<=l)return{i:s,edge:"a"};if(Math.abs(n-m)<=l)return{i:s,edge:"b"}}for(let s=0;s<M.length;s++){let u=$(M[s].start),m=$(M[s].end);if(n>u+l&&n<m-l)return{i:s,edge:"move"}}return null}let it=()=>{let n=y==null?void 0:y.series,i=n?n.timestamps.length:0;return i>1?Math.max(Ne()/(i-1),1):1e3};function H(){w===0&&(w=requestAnimationFrame(gt))}function Oe(){var d;if(!a.onHover||!y)return;if(x<0){a.onHover(null);return}let n=(d=y.scored[x])!=null?d:null,i={index:x,point:n,windowStart:Math.max(0,x-y.params.windowSize),windowEnd:x-1};a.onHover(i)}let we=null,fe=null,Re=n=>{let i=t.getBoundingClientRect();return{x:(n.clientX-i.left)*r,y:(n.clientY-i.top)*r}},kt=n=>L&&n>=Ie(),he=n=>{let i=le(te),d=le(ie),l=8*r;return Math.abs(n-i)<=l?"l":Math.abs(n-d)<=l?"r":n>i&&n<d?"move":"out"};function Ge(n){if(!y)return;let{x:i,y:d}=Re(n);if(we||fe||T)return;if(kt(d)){x!==-1&&(x=-1,Oe(),H());let s=he(i);t.style.cursor=s==="l"||s==="r"?"ew-resize":s==="move"?"grab":"pointer";return}if(b&&X){x!==-1&&(x=-1,Oe()),Z==null&&!me&&(J=G(d),oe(),H()),t.style.cursor="crosshair";return}if(b){x!==-1&&(x=-1,Oe());let s=rt(i,d),u=s&&s.edge==="del"?s.i:-1;u!==Se&&(Se=u,H()),t.style.cursor=s?s.edge==="del"?"pointer":s.edge==="move"?"grab":"ew-resize":"crosshair";return}g&&(t.style.cursor="grab");let l=We(i);l!==x&&(x=l,Oe(),H())}function st(){x!==-1&&(x=-1,Oe(),H())}function at(n){if(!g||!y)return;let{x:i,y:d}=Re(n);if(L&&d>=Ie()){let l=he(i);if(l==="l")fe={type:"l",grab:0,vMin:te,vMax:ie};else if(l==="r")fe={type:"r",grab:0,vMin:te,vMax:ie};else if(l==="move")fe={type:"move",grab:Ee(i),vMin:te,vMax:ie};else{let s=Ee(i),u=se();Ae(s-u/2,s+u/2),fe={type:"move",grab:s,vMin:te,vMax:ie}}t.style.cursor="grabbing",n.preventDefault();return}if(d>D.t*r&&d<t.height-D.b*r-j()){if(b&&X){me={x:i,ts:De(i)},Z==null&&(J=G(d)),oe(),H(),n.preventDefault();return}if(b){let l=De(i),s=rt(i,d);if(s&&s.edge==="del"){Be(M[s.i]),n.preventDefault();return}if(s&&s.edge==="move"){let u=M[s.i];O=u,T={mode:"move",iv:u,grab:l,a0:u.start,b0:u.end,moved:!1}}else if(s){let u=M[s.i];O=u,T={mode:"edge",iv:u,edge:s.edge,moved:!1}}else O=null,T={mode:"new",a:l,b:l,moved:!1};t.style.cursor="crosshair",H(),n.preventDefault();return}we={x:i,vMin:te,vMax:ie},t.style.cursor="grabbing",n.preventDefault()}}function Fe(n){if(!y)return;let{x:i,y:d}=Re(n);if(X&&me){Math.abs(i-me.x)>6*r?Pe={a:me.ts,b:De(i)}:(Pe=null,Z==null&&(J=G(d))),oe(),H();return}if(T){let l=De(i);if(T.mode==="new")T.b=l,Math.abs($(T.b)-$(T.a))>4*r&&(T.moved=!0);else if(T.mode==="edge"){let s=T.iv,u=it();T.edge==="a"?s.start=Q(Math.min(l,s.end-u),I,z):s.end=Q(Math.max(l,s.start+u),I,z),T.moved=!0}else{let s=T.iv,u=T.a0+(l-T.grab),m=T.b0+(l-T.grab);u<I&&(m+=I-u,u=I),m>z&&(u-=m-z,m=z),s.start=Q(u,I,z),s.end=Q(m,I,z),T.moved=!0}H();return}if(we){let l=(i-we.x)*(we.vMax-we.vMin)/(U()||1);Ae(we.vMin-l,we.vMax-l)}else if(fe){let l=Ee(i);fe.type==="l"?Ae(Math.min(l,ie-He()),ie):fe.type==="r"?Ae(te,Math.max(l,te+He())):Ae(fe.vMin+(l-fe.grab),fe.vMax+(l-fe.grab))}}function Mt(n){if(X&&me){let{x:i,y:d}=Re(n);if(Math.abs(i-me.x)>6*r){let l=me.ts,s=De(i);ve={a:Math.min(l,s),b:Math.max(l,s)}}else Z=G(d);me=null,Pe=null,oe(),H();return}if(T){let i=T;if(i.mode==="new")if(i.moved){let d=Q(Math.min(i.a,i.b),I,z),l=Q(Math.max(i.a,i.b),I,z),s={start:d,end:l,label:""};M.push(s),O=s}else O=null;else if(i.iv.start>i.iv.end){let d=i.iv.start;i.iv.start=i.iv.end,i.iv.end=d}T=null,t.style.cursor="crosshair",i.moved&&Ue(),H();return}(we||fe)&&(we=null,fe=null,t.style.cursor="grab")}function St(n){let i=n.target,d=!!i&&(i.tagName==="INPUT"||i.tagName==="TEXTAREA"||i.isContentEditable);X||d||!O||(n.key==="Delete"||n.key==="Backspace"?(n.preventDefault(),Be(O)):n.key==="Escape"&&(O=null,H()))}function $t(n){if(!g||!y)return;n.preventDefault();let{x:i,y:d}=Re(n),l=Q(se()*Math.pow(1.0015,n.deltaY),He(),Ne());if(d>=Ie()){let s=Ee(i);Ae(s-l/2,s+l/2)}else{let s=De(i),u=(s-te)/(se()||1);Ae(s-u*l,s-u*l+l)}}function Rt(){g&&Ae(I,z)}t.addEventListener("mousemove",Ge),t.addEventListener("mouseleave",st),g&&(t.addEventListener("mousedown",at),t.addEventListener("wheel",$t,{passive:!1}),t.addEventListener("dblclick",Rt),window.addEventListener("mousemove",Fe),window.addEventListener("mouseup",Mt)),b&&(window.addEventListener("keydown",St),t.style.cursor="crosshair");function It(n){y=n,!b&&n.incidents&&(M=n.incidents),bt(n.series,n.scored),x>=n.series.timestamps.length&&(x=-1),H()}function Ct(n){K!==n&&(K=n,y&&bt(y.series,y.scored),H())}function Ye(n){M=n,O=null,Se=-1,H()}function zt(n){b&&(X=n,n?O=null:(J=null,me=null,Pe=null),t.style.cursor="crosshair",oe(),H())}function Tt(n){V=n,oe(),H()}function At(n){re=Math.max(0,Math.floor(n)||0),oe(),H()}function Dt(n){Z=n!=null&&xe(n)?n:null,oe(),H()}function Lt(){let n=$e();for(let[i,d]of n)vt(i,d);return n.length&&Ue(),oe(),H(),n.length}function Ht(){ve=null,Pe=null,oe(),H()}function Ft(){return ve?{start:Math.min(ve.a,ve.b),end:Math.max(ve.a,ve.b)}:null}function Bt(n){ve=n?{a:n.start,b:n.end}:null,oe(),H()}function lt(){Qe(),y&&(bt(y.series,y.scored),H())}function Ke(){t.removeEventListener("mousemove",Ge),t.removeEventListener("mouseleave",st),g&&(t.removeEventListener("mousedown",at),t.removeEventListener("wheel",$t),t.removeEventListener("dblclick",Rt),window.removeEventListener("mousemove",Fe),window.removeEventListener("mouseup",Mt)),b&&window.removeEventListener("keydown",St),w!==0&&cancelAnimationFrame(w),w=0,y=null}return Qe(),{render:It,resize:lt,setZeroLine:Ct,setViewWindow:ht,setIncidents:Ye,setThresholdMode:zt,setThresholdDirection:Tt,setThresholdGap:At,setThresholdValue:Dt,applyThreshold:Lt,clearCaptureWindow:Ht,getCaptureWindow:Ft,setCaptureWindow:Bt,destroy:Ke}}var hn={mad:3,zscore:3,iqr:1.5},bn={mad:10,zscore:3,iqr:4},gn="dtk-tune";function p(t,a,h){let e=document.createElement(t);return a&&(e.className=a),h!=null&&(e.textContent=h),e}function pt(t,a){let h=p("label","dtk-ctl-label",t);if(a){h.title=a;let e=p("span","dtk-ctl-info","\u24D8");e.title=a,h.appendChild(document.createTextNode(" ")),h.appendChild(e)}return h}function Nt(t,a,h,e,g){let L=p("div","dtk-ctl");L.appendChild(pt(t,g));let B=p("div","dtk-seg"),b=h,r=[],y=()=>{r.forEach(x=>x.classList.toggle("on",x.dataset.v===b))};return a.forEach(x=>{let w=p("button","dtk-seg-btn",x.label);w.type="button",w.dataset.v=x.value,w.onclick=()=>{b=x.value,y(),e(b)},r.push(w),B.appendChild(w)}),y(),L.appendChild(B),{row:L,get:()=>b,set:x=>{b=x,y()}}}function mt(t,a,h){var y;let e=p("div","dtk-ctl"),g=p("div","dtk-ctl-head"),L=pt(t,a.hint),B=p("span","dtk-ctl-val"),b=(y=a.fmt)!=null?y:(x=>String(x));g.appendChild(L),g.appendChild(B),e.appendChild(g);let r=p("input","dtk-range");return r.type="range",r.min=String(a.min),r.max=String(a.max),r.step=String(a.step),r.value=String(a.value),B.textContent=b(a.value),r.oninput=()=>{let x=Number(r.value);B.textContent=b(x),h(x)},e.appendChild(r),{row:e,get:()=>Number(r.value),setMax:x=>{r.max=String(x),Number(r.value)>x&&(r.value=String(x),B.textContent=b(x))}}}function ln(t){if(t.type==="manual_bounds"){let h={};return t.lowerBound!=null&&(h.lower_bound=t.lowerBound),t.upperBound!=null&&(h.upper_bound=t.upperBound),t.inputType!=="values"&&(h.input_type=t.inputType),h}let a={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(a.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(a.half_life=t.halfLife)),t.detrend!=="none"&&(a.detrend=t.detrend),t.smoothing!=="none"&&(a.smoothing=t.smoothing),t.inputType!=="values"&&(a.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(a.seasonality_components=t.seasonalityComponents,a.min_samples_per_group=t.minSamplesPerGroup),a}function vn(t,a){let h=ln(t),e=[`type: ${t.type}`];for(let[g,L]of Object.entries(h))e.push(`${g}=${typeof L=="object"?JSON.stringify(L):L}`);return t.direction&&t.direction!=="any"&&e.push(`direction=${t.direction}`),e.push(`consecutive_anomalies=${a}`),e.join(" \xB7 ")}function xn(t,a){var qe,Xt,Zt;yn(),a.classList.add(gn),a.innerHTML="";let h=p("div","dtk-tune-root");a.appendChild(h);let e=t.points.length,g={timestamps:t.points.map(o=>o.t),values:t.points.map(o=>o.v==null?NaN:o.v),intervalSeconds:t.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:t.seasonality_columns.length?t.seasonality:void 0,seasonalityColumns:t.seasonality_columns.length?t.seasonality_columns:void 0},L=g,B=o=>{let c=Math.max(0,e-o);return c<=0?g:{timestamps:g.timestamps.slice(c),values:g.values.slice(c),intervalSeconds:g.intervalSeconds,truthAnomaly:g.truthAnomaly.slice(c),seasonalityData:g.seasonalityData?g.seasonalityData.slice(c):void 0,seasonalityColumns:g.seasonalityColumns}},b=o=>Date.parse(o.replace(" ","T")+"Z"),r=(t.incidents||[]).map(o=>({start:b(o.start),end:b(o.end),label:o.label||""})).filter(o=>Number.isFinite(o.start)&&Number.isFinite(o.end)).map(o=>({start:Math.min(o.start,o.end),end:Math.max(o.start,o.end),label:o.label})),y=(()=>{let o=(t.capture_windows||[])[0];if(!o)return null;let c=b(o.start),f=b(o.end);return!Number.isFinite(c)||!Number.isFinite(f)?null:{start:Math.min(c,f),end:Math.max(c,f)}})(),x=[],w=t.detector,K=t.consecutive_anomalies,M=g.values.filter(o=>Number.isFinite(o)).sort((o,c)=>o-c),O=M.length?M[0]:0,Se=M.length?M[M.length-1]:1,T=o=>M.length?M[Math.min(M.length-1,Math.max(0,Math.round(o*(M.length-1))))]:0,ee=Math.max((Se-O)*.05,1e-9),X=O-ee,V=Se+ee,re=Math.max((V-X)/400,1e-9),Z=w.lowerBound!=null?w.lowerBound:T(.05),J=w.upperBound!=null?w.upperBound:T(.95),ve=(qe=w.seasonalityComponents)!=null?qe:[],me=new Map;ve.forEach((o,c)=>o.forEach(f=>me.set(f,c+1)));let Pe=()=>{let o=0;me.forEach(f=>{f>o&&(o=f)});let c=[];for(let f=1;f<=o;f++){let A=t.seasonality_columns.filter(E=>me.get(E)===f);A.length&&c.push(A)}return c.length?c:null},I=o=>{if(!o||!t.seasonality.length)return 0;let c=0;for(let f of o){let A=new Set;for(let E of t.seasonality)A.add(f.map(Y=>{var Le;return String((Le=E==null?void 0:E[Y])!=null?Le:"")}).join("|"));c=Math.max(c,A.size)}return c},z=()=>{var o;return{type:Ge.get(),threshold:Fe.get(),windowSize:Ct.get(),minSamples:w.minSamples,inputType:w.inputType,smoothing:Dt.get(),smoothingAlpha:w.smoothingAlpha,smoothingWindow:w.smoothingWindow,windowWeights:Ye.get(),halfLife:Ye.get()==="exponential"?zt.get():null,detrend:At.get(),seasonalityComponents:Pe(),minSamplesPerGroup:(o=bn[Ge.get()])!=null?o:w.minSamplesPerGroup,consecutiveAnomalies:K,direction:Ht.get(),lowerBound:st.get(),upperBound:at.get()}},pe=p("div","dtk-tune-header"),Ce=p("div","dtk-tune-titlerow");Ce.appendChild(p("h1","dtk-tune-title",t.metric));let te=p("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");Ce.appendChild(te),pe.appendChild(Ce);let ie=t.project?`${t.project} \xB7 `:"";pe.appendChild(p("div","dtk-tune-sub",`${ie}${e} points \xB7 ${wn(t.interval_seconds)} grid`)),t.description&&pe.appendChild(p("div","dtk-tune-desc",t.description)),h.appendChild(pe);let ft=p("div","dtk-tune-metrics");h.appendChild(ft);let Qe=p("div","dtk-tune-grid"),j=p("div","dtk-tune-controls"),U=p("div","dtk-tune-main");Qe.appendChild(j),Qe.appendChild(U),h.appendChild(Qe);let ye=p("div","dtk-tune-trim"),Te=p("div","dtk-tune-trim-head");Te.appendChild(pt("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 Ve=p("span","dtk-tune-trim-val");Te.appendChild(Ve),ye.appendChild(Te);let se=p("input","dtk-range");se.type="range",se.min=String(Math.min(e,200)),se.max=String(e),se.step=String(Math.max(1,Math.round(e/200))),se.value=String(e),ye.appendChild(se),U.appendChild(ye);let Ne=p("div","dtk-tune-chart"),$=p("canvas");Ne.appendChild($);let ne=p("div","dtk-tune-spin");ne.appendChild(p("span","dtk-spin-ring")),ne.appendChild(p("span","dtk-spin-txt","computing\u2026")),Ne.appendChild(ne),U.appendChild(Ne);let Ie=p("div","dtk-tune-legend"),ae=(o,c,f)=>{let A=p("span","dtk-leg-item");A.title=f,A.appendChild(p("span",`dtk-leg-sw ${o}`)),A.appendChild(p("span","dtk-leg-txt",c)),Ie.appendChild(A)};ae("line","metric","The metric value over time."),ae("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),ae("center","band center","The expected value at the middle of the band."),ae("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),ae("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),U.appendChild(Ie);let ze=p("div","dtk-tune-readout");U.appendChild(ze);let le=p("div","dtk-tune-warn");le.style.display="none",U.appendChild(le);let Ot=o=>{let c=o.seasonalityComponents,f=I(c),A=o.minSamplesPerGroup*f;c&&f>0&&o.windowSize<A?(le.textContent=`\u26A0 Seasonality inactive at this window: ${o.windowSize} < ${A} (min_samples_per_group ${o.minSamplesPerGroup} \xD7 ${f} key${f===1?"":"s"}). Each point keeps only ~${Math.floor(o.windowSize/f)} same-key point(s), so the band falls back to global statistics (seasonality has no effect). Raise the window to \u2265 ${A}.`,le.style.display=""):le.style.display="none"},et=p("div","dtk-tune-labhead");et.appendChild(pt("Real incidents","Drag on this chart to mark each real incident span \u2014 drag its edges to adjust, drag the middle to move, click its \u2715 (or select + Delete) to remove. Pan via the strip below, scroll to zoom (both charts move together). The metrics above update as you tune.")),et.appendChild(p("span","dtk-tune-labhint","drag to mark \xB7 edges adjust \xB7 \u2715/Delete remove \xB7 strip pans")),U.appendChild(et);let Q=p("div","dtk-tune-chart dtk-tune-labchart"),He=p("canvas");Q.appendChild(He),U.appendChild(Q);let Ae=(o,c)=>{let f=t.interval_seconds*1e3/2,A=L.timestamps,E=(A.length?A[0]:0)-f,Y=(A.length?A[A.length-1]:0)+f,Le=c.filter(be=>be.end>=E&&be.start<=Y),Pt=(be,ut)=>be>=ut.start-f&&be<=ut.end+f,dt=0;for(let be of o)Le.some(ut=>Pt(be,ut))&&dt++;let ct=0;for(let be of Le)o.some(ut=>Pt(ut,be))&&ct++;let ue=o.length;return{realIncidents:Le.length,caught:ct,recall:Le.length?ct/Le.length:NaN,totalAlerts:ue,correctAlerts:dt,falseAlerts:ue-dt,fdr:ue?(ue-dt)/ue:NaN}},ht=o=>Number.isFinite(o)?`${Math.round(o*100)}%`:"\u2014",De=(o,c,f,A)=>`<span class="dtk-m-chip"><span class="dtk-m-dot" style="background:${A}"></span><span class="dtk-m-v">${o}</span><span class="dtk-m-l">${c}</span>`+(f?`<span class="dtk-m-sub">${f}</span>`:"")+"</span>";function Ee(){let o=Ae(x,r),c=o.realIncidents>0,f;c?o.totalAlerts===0?f="no alerts":o.falseAlerts===0?f=`${ht(o.fdr)} \xB7 all correct`:f=`${ht(o.fdr)} \xB7 \u22481 in ${Math.max(1,Math.round(o.totalAlerts/o.falseAlerts))} false`:f="mark incidents to measure",ft.innerHTML=De(String(o.realIncidents),"real incidents","","var(--c)")+De(c?`${o.caught}/${o.realIncidents}`:"\u2014","caught",c?`recall ${ht(o.recall)}`:"mark incidents","var(--green)")+De(String(o.totalAlerts),"alerts","","var(--c)")+De(c?String(o.falseAlerts):"\u2014","false alerts",f,"var(--anom)")}let G,We=Yt($,{navigable:!0,showNavigator:!1,yFit:"data",onViewChange:(o,c)=>G.setViewWindow(o,c),onHover:o=>{if(!o||!o.point||!o.point.scored){ze.textContent="";return}let c=o.point;ze.textContent=`t=${Kt(c.timestamp)} value=${Et(c.value)} band=[${Et(c.lower)}, ${Et(c.upper)}]`+(c.isAnomaly?` \u26A0 ${c.direction} (sev ${c.severity.toFixed(2)})`:"")}}),bt={type:"mad",threshold:3,windowSize:100,minSamples:30,inputType:"values",smoothing:"none",smoothingAlpha:.3,smoothingWindow:10,windowWeights:"none",halfLife:null,detrend:"none",seasonalityComponents:null,minSamplesPerGroup:10,consecutiveAnomalies:1},tt=()=>{};G=Yt(He,{navigable:!0,labeling:!0,yFit:"data",onViewChange:(o,c)=>We.setViewWindow(o,c),onIncidentsChange:()=>{s(),Ee(),We.setIncidents(r)},onThresholdChange:o=>tt(o)}),G.setIncidents(r),y&&G.setCaptureWindow(y);let _t=()=>{G.render({series:L,scored:[],params:bt,alerts:[]})},gt=p("div","dtk-th"),je=p("button","dtk-th-toggle","Threshold capture");je.type="button",je.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.",gt.appendChild(je);let ke=p("div","dtk-th-bar"),nt=p("select","dtk-th-sel");for(let[o,c]of[["above","above the line"],["below","below the line"]]){let f=p("option");f.value=o,f.textContent=c,nt.appendChild(f)}let _e=p("input","dtk-th-num");_e.type="number",_e.step="any",_e.placeholder="hover chart";let Me=p("input","dtk-th-num");Me.type="number",Me.min="0",Me.step="1",Me.value="0";let ot=p("span","dtk-th-scope"),$e=p("button","dtk-inc-btn","\u21BA whole view");$e.type="button",$e.style.display="none";let oe=p("button","dtk-apply-btn dtk-th-add","Add 0 spans");oe.type="button",oe.disabled=!0;let vt=p("button","dtk-inc-btn","Done");vt.type="button";let xt=(o,c)=>{let f=p("label","dtk-th-grp");return f.appendChild(p("span","dtk-th-lbl",o)),f.appendChild(c),f};ke.appendChild(xt("grab points",nt)),ke.appendChild(xt("line value",_e)),ke.appendChild(xt("bridge gaps \u2264",Me)),ke.appendChild(ot),ke.appendChild($e),ke.appendChild(oe),ke.appendChild(vt),ke.style.display="none",gt.appendChild(ke),U.insertBefore(gt,Q);let wt=!1,yt=!1,Ue=o=>{wt=o,je.classList.toggle("on",o),ke.style.display=o?"flex":"none",G.setThresholdMode(o)};je.onclick=()=>Ue(!wt),vt.onclick=()=>Ue(!1),nt.onchange=()=>G.setThresholdDirection(nt.value),_e.oninput=()=>{let o=_e.value.trim();yt=!0,G.setThresholdValue(o!==""&&!isNaN(Number(o))?Number(o):null),yt=!1},Me.oninput=()=>G.setThresholdGap(Number(Me.value)||0),$e.onclick=()=>G.clearCaptureWindow(),oe.onclick=()=>{G.applyThreshold()},window.addEventListener("keydown",o=>{o.key==="Escape"&&wt&&Ue(!1)}),tt=o=>{oe.textContent=`Add ${o.runs} span${o.runs===1?"":"s"}`,oe.disabled=o.runs===0,o.locked&&o.value!=null&&!yt&&(_e.value=String(Math.round(o.value*1e3)/1e3)),$e.style.display=o.committed?"":"none",ot.textContent=o.window?`scope: ${Wt(o.windowMs)} painted`:"scope: current view \u2014 drag the chart to limit it"};let Be=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function j(e,n,t){let o=un(e),i=o.map(b=>e[b]),r=o.map(b=>n[b]),s=C(r),c=new Array(r.length),u=0;for(let b=0;b<r.length;b++)u+=r[b],c[b]=(u-.5*r[b])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return j(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(a=>a>o),r=i.map(a=>!a),s=0,c=0;for(let a=0;a<n.length;a++)i[a]?s++:c++;if(s<2||c<2)return 0;let u=(a,M)=>M.filter((x,w)=>a[w]),d=u(r,e),b=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),m=u(i,t),A=P(d,h),p=P(b,m),S=P(N,h),g=P(y,m);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=j(n,t,25),i=j(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function F(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,b,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(b=n.upperBound)!=null?b:null,u=[];for(let y=0;y<i;y++){let h=o[y],m=r[y],A=t[y];if(Number.isNaN(m)){u.push(F(y,A,h,m,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&m<s&&(p=!0,S="below",g=s-m),c!==null&&m>c&&(p=!0,S="above",g=m-c);let a=0;if(p)if(s!==null&&c!==null){let v=c-s;a=v>0?g/v:1/0}else a=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:m,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:a,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),b=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let m=0;m<c;m++){let A=s[m],p=d[m],S=r[m];if(Number.isNaN(p)){h.push(F(m,S,A,p,"missing_data"));continue}let g=Math.max(0,m-n.windowSize),a=m-g,M=d.slice(g,m),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(F(m,S,A,p,"insufficient_data"));continue}let v=new Array(a);for(let l=0;l<a;l++)v[l]=a-l;let L=[];for(let l=0;l<a;l++)x[l]&&L.push(v[l]);let B=Q(L,y),W=0;n.detrend==="linear"&&(W=Sn(w,L,B));let nn=W!==0?w.map((l,R)=>l+W*L[R]):w,G=U(t,nn,B),_={...G};if(b&&N){let l=N[m];for(let R of n.seasonalityComponents){let K=new Array(a).fill(!0);for(let f=0;f<a;f++){let q=N[g+f],T=!0;for(let V of R)if(!q||q[V]!==(l==null?void 0:l[V])){T=!1;break}K[f]=T}let z=[],E=[];for(let f=0;f<a;f++)x[f]&&K[f]&&(z.push(M[f]),E.push(v[f]));if(z.length<n.minSamplesPerGroup)continue;let tn=Q(E,y),on=W!==0?z.map((f,q)=>f+W*E[q]):z,rn=U(t,on,tn);for(let[f,q]of i){let T=G[f],sn=(q==="spread"?T>0:T!==0)?rn[f]/T:1;_[f]*=sn}}}let[D,k]=Nn(t,_,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,O=null,J=0;if(H){let l;p<D?(O="below",l=D-p):(O="above",l=p-k),J=An(t,_,l)}let en=Mn(t,_);h.push({index:m,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:O,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var I=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function vn(e,n,t){let o=[],i=0;for(let r=0;r<e.length;r++){if(!(e[r].scored&&e[r].isAnomaly)){i=0;continue}let c=r>0&&e[r-1].scored&&e[r-1].isAnomaly,u=r>0&&e[r].timestamp-e[r-1].timestamp===n;i=c&&u?i+1:1,i===t&&o.push(r)}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){I=n.series;return}if(n.type==="run"&&I){let t=n.params,o=Z(I,t);xn(o,t.direction);let i=I.intervalSeconds*1e3,r=vn(o,i,t.consecutiveAnomalies),s=$(I,t),c=0;for(let u of o)u.scored&&u.isAnomaly&&c++;self.postMessage({type:"result",id:n.id,scored:o,fires:r,eff:s,flagged:c})}};})();
|
|
2
|
+
`],{type:"text/javascript"})));Be.postMessage({type:"series",series:L});let rt=0,it=null;Be.onmessage=o=>{let c=o.data;if(c.type!=="result"||c.id!==rt||!it)return;ne.classList.remove("on");let f=it,A=c.fires.map(Y=>L.timestamps[Y]),E=A.map(Y=>({t:Y,kind:"anomaly"}));We.render({series:L,scored:c.scored,params:f,alerts:E,incidents:r}),x=A,Ee(),v.textContent=`${c.flagged} flagged \xB7 ${c.fires.length} alert${c.fires.length===1?"":"s"} \xB7 warm-up ${c.eff} pts`,Ot(f),P.textContent=vn(f,K)},Be.onerror=()=>{ne.classList.remove("on"),v.textContent="recompute failed \u2014 see the browser console"};let H=()=>{it=z(),rt+=1,ne.classList.add("on"),Be.postMessage({type:"run",id:rt,params:it})},Oe=o=>o>1?Wt(g.timestamps[e-1]-g.timestamps[e-o]):"\u2014",we=o=>{Ve.textContent=o>=e?`${e} pts \xB7 full (${Oe(e)})`:`${o} pts \xB7 ${Oe(o)}`};we(e);function fe(o){L=B(o),Be.postMessage({type:"series",series:L}),_t(),he()}let Re=0;se.oninput=()=>{let o=Number(se.value);we(o),Re&&window.clearTimeout(Re),Re=window.setTimeout(()=>fe(o),200)};let kt=0,he=()=>{kt&&window.clearTimeout(kt),kt=window.setTimeout(H,130)},Ge=Nt("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],w.type,o=>{var c;$t((c=hn[o])!=null?c:3),m(),he()},"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).");j.appendChild(Ge.row);let st=mt("Lower bound",{min:X,max:V,step:re,value:Z,fmt:o=>Et(o),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)."},he);j.appendChild(st.row);let at=mt("Upper bound",{min:X,max:V,step:re,value:J,fmt:o=>Et(o),hint:"Manual bounds: values above this read as anomalous."},he);j.appendChild(at.row);let Fe=mt("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:w.threshold,fmt:o=>o.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},he);j.appendChild(Fe.row);let Mt=Fe.row.querySelector("input"),St=Fe.row.querySelector(".dtk-ctl-val"),$t=o=>{Mt&&(Mt.value=String(o)),St&&(St.textContent=o.toFixed(1))},Rt=Math.max(50,Math.min(2e3,Math.floor(e/2))),It=Math.max(Rt,w.windowSize),Ct=mt("Window size (points)",{min:Math.max(1,Math.min(10,w.windowSize)),max:It,step:1,value:w.windowSize,fmt:o=>`${o} \xB7 ${Wt(o*t.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},he);j.appendChild(Ct.row);let Ye=Nt("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],w.windowWeights,()=>{m(),he()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");j.appendChild(Ye.row);let zt=mt("Half-life (points)",{min:1,max:It,step:1,value:(Xt=w.halfLife)!=null?Xt:Math.max(5,Math.round(w.windowSize/20)),fmt:o=>`${o} \xB7 ${Wt(o*t.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},he),Tt=zt.row;Tt.style.display=w.windowWeights==="exponential"?"":"none",j.appendChild(Tt);let At=Nt("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],w.detrend,he,"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.");j.appendChild(At.row);let Dt=Nt("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],w.smoothing,he,"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.");j.appendChild(Dt.row);let Lt=null;if(t.seasonality_columns.length){let o=t.seasonality_columns,c=p("div","dtk-ctl");Lt=c,c.appendChild(pt("Seasonality groups","Condition the band on seasonal keys. Pick a group per column: columns in the SAME group are combined into one key (e.g. dow\xD7hour); separate groups each apply their own correction. Off = ignore that column."));let f=Math.min(o.length,6),A=[{label:"\u2014",value:"0"}];for(let E=1;E<=f;E++)A.push({label:`G${E}`,value:String(E)});o.forEach(E=>{let Y=p("div","dtk-season-row");Y.appendChild(p("span","dtk-season-col",E));let Le=p("div","dtk-seg dtk-season-seg"),Pt=[],dt=()=>{var ue;return(ue=me.get(E))!=null?ue:0},ct=()=>Pt.forEach(ue=>ue.classList.toggle("on",Number(ue.dataset.v)===dt()));A.forEach(ue=>{let be=p("button","dtk-seg-btn",ue.label);be.type="button",be.dataset.v=ue.value,be.title=ue.value==="0"?`ignore ${E}`:`put ${E} in group ${ue.value}`,be.onclick=()=>{me.set(E,Number(ue.value)),ct(),he()},Pt.push(be),Le.appendChild(be)}),ct(),Y.appendChild(Le),c.appendChild(Y)}),j.appendChild(c)}let Ht=Nt("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(Zt=t.direction)!=null?Zt:"any",he,"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.");j.appendChild(Ht.row);let Ft=mt("Alert: consecutive anomalies",{min:1,max:10,step:1,value:K,fmt:o=>String(o),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},o=>{K=o,he()});j.appendChild(Ft.row);let Bt=p("div","dtk-ctl"),lt=p("label","dtk-check"),Ke=p("input");Ke.type="checkbox",Ke.onchange=()=>{We.setZeroLine(Ke.checked),G.setZeroLine(Ke.checked)},lt.title="Draw a horizontal line at y = 0 and include zero in the scale \u2014 for real-valued metrics best read relative to zero.",lt.appendChild(Ke),lt.appendChild(document.createTextNode(" Show y = 0 line")),Bt.appendChild(lt),j.appendChild(Bt);let n=p("div","dtk-ctl dtk-incidents");n.appendChild(pt("Marked incidents","The real incidents marked on the lower chart. Edit a label, focus to zoom both charts to it, or remove it. Save the set below to incidents/<metric>/ \u2014 the same store dtk autotune reads."));let i=p("div","dtk-inc-list");n.appendChild(i),j.appendChild(n);function d(o){let c=Math.max((o.end-o.start)*.5,t.interval_seconds*1e3*5);We.setViewWindow(o.start-c,o.end+c),G.setViewWindow(o.start-c,o.end+c)}function l(o){let c=r.indexOf(o);c>=0&&r.splice(c,1),G.setIncidents(r),We.setIncidents(r),s(),Ee()}function s(){i.innerHTML="";let o=[...r].sort((c,f)=>c.start-f.start);if(!o.length){i.appendChild(p("div","dtk-inc-empty","None yet \u2014 drag across the lower chart to mark one."));return}for(let c of o){let f=p("div","dtk-inc-row");f.appendChild(p("span","dtk-inc-span",`${Kt(c.start)} \u2192 ${Kt(c.end)}`)),f.appendChild(p("span","dtk-inc-dur",Wt(Math.max(0,c.end-c.start))));let A=p("input","dtk-inc-label");A.type="text",A.value=c.label||"",A.placeholder="label (optional)",A.oninput=()=>{c.label=A.value},f.appendChild(A);let E=p("button","dtk-inc-btn","focus");E.type="button",E.onclick=()=>d(c),f.appendChild(E);let Y=p("button","dtk-inc-btn dtk-inc-del","\u2715");Y.type="button",Y.title="remove this incident",Y.onclick=()=>l(c),f.appendChild(Y),i.appendChild(f)}}let u=[Fe.row,Ct.row,Ye.row,At.row,Dt.row];Lt&&u.push(Lt);function m(){let o=Ge.get()==="manual_bounds";for(let c of u)c.style.display=o?"none":"";st.row.style.display=o?"":"none",at.row.style.display=o?"":"none",Tt.style.display=!o&&Ye.get()==="exponential"?"":"none"}m();let v=p("div","dtk-tune-stat");U.appendChild(v);let W=p("div","dtk-tune-cfg");W.appendChild(p("span","dtk-tune-cfg-k","// effective config"));let P=p("code","dtk-tune-cfg-v");if(W.appendChild(P),U.appendChild(W),t.save_url){let o=p("div","dtk-tune-apply"),c=p("button","dtk-apply-btn","Apply to metric");c.type="button",c.title="Write this detector config back into the metric YAML (the previous version is archived under metrics/.history/). Trimming the sample does not change what is written.";let f=p("span","dtk-apply-msg");c.onclick=()=>{let A=z();c.disabled=!0,f.className="dtk-apply-msg info",f.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:A.type,params:ln(A)},consecutive_anomalies:K})}).then(E=>E.ok?E.json():E.text().then(Y=>{throw new Error(Y||`HTTP ${E.status}`)})).then(E=>{var Y;f.className="dtk-apply-msg ok",f.textContent=`Applied \u2192 ${(Y=E.saved)!=null?Y:"metric"} (previous archived). You can close this tab.`}).catch(E=>{c.disabled=!1,f.className="dtk-apply-msg err",f.textContent=`Apply failed: ${E.message}`})},o.appendChild(c),o.appendChild(f),U.appendChild(o)}else U.appendChild(p("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));let _=o=>'"'+o.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',F=o=>new Date(o).toISOString().slice(0,19).replace("T"," "),de=()=>{let o=[`metric: ${t.metric}`,"timezone: UTC"],c=[...r].sort((A,E)=>A.start-E.start);if(!c.length)o.push("incidents: []");else{o.push("incidents:");for(let A of c){let E=A.label?`, label: ${_(A.label)}`:"";o.push(` - {start: "${F(A.start)}", end: "${F(A.end)}"${E}}`)}}let f=G.getCaptureWindow();return f&&(o.push("capture_windows:"),o.push(` - {start: "${F(f.start)}", end: "${F(f.end)}"}`)),o.join(`
|
|
3
|
+
`)+`
|
|
4
|
+
`},k=p("div","dtk-tune-apply"),C=p("input","dtk-setname");C.type="text",C.placeholder="name this set (optional)";let S=p("button","dtk-apply-btn dtk-labels-btn",t.labels_save_url?"Save incidents":"Download incidents");S.type="button",S.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 N=p("span","dtk-apply-msg");S.onclick=()=>{let o=de();if(!t.labels_save_url){let c=new Blob([o],{type:"text/yaml"}),f=p("a");f.href=URL.createObjectURL(c),f.download=`${t.metric}.yml`,f.click(),URL.revokeObjectURL(f.href),N.className="dtk-apply-msg ok",N.textContent=`Downloaded \u2014 drop it into incidents/${t.metric}/`;return}N.className="dtk-apply-msg info",N.textContent="Saving\u2026",fetch(t.labels_save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:C.value,yaml:o})}).then(c=>c.ok?c.json():c.text().then(f=>{throw new Error(f||`HTTP ${c.status}`)})).then(c=>{var f;N.className="dtk-apply-msg ok",N.textContent=`Saved \u2192 ${(f=c.saved)!=null?f:"incidents"} (keep tuning, or Apply the detector)`}).catch(c=>{N.className="dtk-apply-msg err",N.textContent=`Save failed: ${c.message}`})},k.appendChild(C),k.appendChild(S),k.appendChild(N),U.appendChild(k),_t(),s(),H(),Ee();let ce=0;window.addEventListener("resize",()=>{ce&&cancelAnimationFrame(ce),ce=requestAnimationFrame(()=>{We.resize(),G.resize()})})}function Et(t){if(!Number.isFinite(t))return"\u2014";let a=Math.abs(t);return a!==0&&(a<.01||a>=1e6)?t.toExponential(2):Number(t.toFixed(a<1?4:2)).toString()}function Kt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function Wt(t){let a=Math.round(t/6e4);if(a<60)return`${a}m`;let h=Math.floor(a/60);if(h<24)return`${h}h`;let e=Math.floor(h/24),g=h%24;return g?`${e}d ${g}h`:`${e}d`}function wn(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var an=!1;function yn(){if(an)return;an=!0;let t=`
|
|
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:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
|
|
10
|
+
.dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
|
|
11
|
+
.dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
|
|
12
|
+
.dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
|
|
13
|
+
color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
|
|
14
|
+
.dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
|
|
15
|
+
.dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
|
|
16
|
+
.dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
|
|
17
|
+
@media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
|
|
18
|
+
.dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
|
|
19
|
+
border:1px solid var(--border);border-radius:12px;padding:16px;}
|
|
20
|
+
.dtk-ctl{display:flex;flex-direction:column;gap:6px;}
|
|
21
|
+
.dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
|
|
22
|
+
.dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
|
|
23
|
+
.dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
|
|
24
|
+
.dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
|
|
25
|
+
.dtk-seg.dtk-wrap{flex-wrap:wrap;}
|
|
26
|
+
.dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
|
|
27
|
+
font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
|
|
28
|
+
.dtk-seg-btn:hover{color:var(--ink);}
|
|
29
|
+
.dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
|
|
30
|
+
.dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
|
|
31
|
+
.dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
|
|
32
|
+
.dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
|
|
33
|
+
.dtk-tune-chart{position:relative;width:100%;height:470px;background:var(--surface);
|
|
34
|
+
border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
|
35
|
+
.dtk-tune-chart canvas{width:100%;height:100%;display:block;}
|
|
36
|
+
.dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
|
|
37
|
+
.dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
|
|
38
|
+
.dtk-tune-warn{font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--c7);
|
|
39
|
+
background:rgba(240,173,78,0.13);border:1px solid rgba(240,173,78,0.5);border-radius:8px;padding:8px 11px;}
|
|
40
|
+
.dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
|
|
41
|
+
font-size:12px;overflow-x:auto;}
|
|
42
|
+
.dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
|
|
43
|
+
.dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
|
|
44
|
+
.dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
|
|
45
|
+
.dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
|
|
46
|
+
font-size:14px;font-weight:600;cursor:pointer;}
|
|
47
|
+
.dtk-apply-btn:hover{background:var(--c7);}
|
|
48
|
+
.dtk-apply-btn:disabled{opacity:.55;cursor:default;}
|
|
49
|
+
.dtk-apply-msg{font-size:13px;}
|
|
50
|
+
.dtk-apply-msg.ok{color:var(--green);}
|
|
51
|
+
.dtk-apply-msg.err{color:var(--anom);}
|
|
52
|
+
.dtk-apply-msg.info{color:var(--muted);}
|
|
53
|
+
.dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
|
|
54
|
+
border-radius:8px;padding:10px 12px;}
|
|
55
|
+
.dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
|
|
56
|
+
.dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
|
|
57
|
+
border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
|
|
58
|
+
.dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
|
|
59
|
+
.dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
|
|
60
|
+
.dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
|
|
61
|
+
background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
|
|
62
|
+
padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
|
|
63
|
+
.dtk-tune-spin.on{display:inline-flex;}
|
|
64
|
+
.dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
|
|
65
|
+
border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
|
|
66
|
+
@keyframes dtk-spin{to{transform:rotate(360deg);}}
|
|
67
|
+
.dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
|
|
68
|
+
.dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
|
|
69
|
+
.dtk-leg-sw{display:inline-block;flex:0 0 auto;}
|
|
70
|
+
.dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
|
|
71
|
+
.dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
|
|
72
|
+
border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
|
|
73
|
+
.dtk-leg-sw.center{width:16px;height:2px;
|
|
74
|
+
background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
|
|
75
|
+
.dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
|
|
76
|
+
.dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
|
|
77
|
+
border-top:7px solid var(--anom);}
|
|
78
|
+
.dtk-leg-txt{white-space:nowrap;}
|
|
79
|
+
.dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
80
|
+
.dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
|
|
81
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
82
|
+
.dtk-season-seg{flex:0 0 auto;padding:2px;}
|
|
83
|
+
.dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
|
|
84
|
+
.dtk-tune-metrics{display:flex;flex-wrap:wrap;gap:10px;margin:16px 0 2px;}
|
|
85
|
+
.dtk-m-chip{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;background:var(--surface);
|
|
86
|
+
border:1px solid var(--border);border-radius:10px;font-size:13px;}
|
|
87
|
+
.dtk-m-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;}
|
|
88
|
+
.dtk-m-v{font-family:var(--mono);font-weight:700;font-size:15px;color:var(--ink);}
|
|
89
|
+
.dtk-m-l{color:var(--faint);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
|
|
90
|
+
.dtk-m-sub{color:var(--muted);font-family:var(--mono);font-size:11.5px;}
|
|
91
|
+
.dtk-tune-labhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
|
|
92
|
+
margin-top:4px;flex-wrap:wrap;}
|
|
93
|
+
.dtk-tune-labhint{font-family:var(--mono);font-size:11px;color:var(--faint);}
|
|
94
|
+
.dtk-tune-labchart{height:240px;}
|
|
95
|
+
.dtk-th{display:flex;flex-direction:column;gap:8px;margin:2px 0 6px;}
|
|
96
|
+
.dtk-th-toggle{align-self:flex-start;border:1px solid var(--border);background:var(--surface);
|
|
97
|
+
color:var(--muted);border-radius:8px;padding:6px 12px;font-family:var(--sans);font-size:12.5px;cursor:pointer;}
|
|
98
|
+
.dtk-th-toggle:hover{border-color:var(--c);color:var(--c7);}
|
|
99
|
+
.dtk-th-toggle.on{background:var(--c);border-color:var(--c);color:#fff;}
|
|
100
|
+
.dtk-th-bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 14px;padding:11px 13px;
|
|
101
|
+
background:var(--surface);border:1px solid var(--border);border-radius:10px;}
|
|
102
|
+
.dtk-th-grp{display:flex;flex-direction:column;gap:3px;}
|
|
103
|
+
.dtk-th-lbl{font-family:var(--mono);font-size:10.5px;color:var(--faint);text-transform:uppercase;letter-spacing:.05em;}
|
|
104
|
+
.dtk-th-num,.dtk-th-sel{background:var(--paper);color:var(--ink);border:1px solid var(--border);
|
|
105
|
+
border-radius:6px;padding:5px 8px;font-family:var(--mono);font-size:12px;}
|
|
106
|
+
.dtk-th-num{width:96px;}
|
|
107
|
+
.dtk-th-num:focus,.dtk-th-sel:focus{outline:none;border-color:var(--c);}
|
|
108
|
+
.dtk-th-scope{font-family:var(--mono);font-size:11px;color:var(--muted);align-self:center;flex:1 1 160px;}
|
|
109
|
+
.dtk-th-add{padding:7px 14px;}
|
|
110
|
+
.dtk-th-add:disabled{opacity:.5;cursor:default;}
|
|
111
|
+
.dtk-incidents{gap:8px;}
|
|
112
|
+
.dtk-inc-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow:auto;}
|
|
113
|
+
.dtk-inc-empty{font-size:12px;color:var(--faint);font-style:italic;}
|
|
114
|
+
.dtk-inc-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:var(--paper);
|
|
115
|
+
border:1px solid var(--border);border-radius:7px;padding:6px 8px;}
|
|
116
|
+
.dtk-inc-span{font-family:var(--mono);font-size:10.5px;color:var(--ink);}
|
|
117
|
+
.dtk-inc-dur{font-family:var(--mono);font-size:10.5px;color:var(--muted);}
|
|
118
|
+
.dtk-inc-label{flex:1 1 90px;min-width:70px;background:var(--surface);color:var(--ink);
|
|
119
|
+
border:1px solid var(--border);border-radius:5px;padding:4px 7px;font-family:var(--sans);font-size:11.5px;}
|
|
120
|
+
.dtk-inc-label:focus{outline:none;border-color:var(--c);}
|
|
121
|
+
.dtk-inc-btn{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;
|
|
122
|
+
padding:3px 8px;font-size:11px;cursor:pointer;font-family:var(--sans);}
|
|
123
|
+
.dtk-inc-btn:hover{border-color:var(--c);color:var(--c7);}
|
|
124
|
+
.dtk-inc-del{color:var(--anom);}
|
|
125
|
+
.dtk-setname{background:var(--surface);color:var(--ink);border:1px solid var(--border);border-radius:8px;
|
|
126
|
+
padding:9px 11px;font-family:var(--sans);font-size:13px;min-width:180px;}
|
|
127
|
+
.dtk-setname::placeholder{color:var(--faint);}
|
|
128
|
+
.dtk-setname:focus{outline:none;border-color:var(--c);}
|
|
129
|
+
.dtk-labels-btn{background:var(--surface);color:var(--ink);border:1px solid var(--border);}
|
|
130
|
+
.dtk-labels-btn:hover{background:var(--paper);border-color:var(--c);color:var(--c7);}
|
|
131
|
+
`,a=document.createElement("style");a.textContent=t,document.head.appendChild(a)}window.__DTK_TUNE__={render:xn};})();
|
|
@@ -35,6 +35,11 @@ _TUNE_MAX_POINTS = 15000
|
|
|
35
35
|
# scored region (the band never reaches its real width). Floor the shown points
|
|
36
36
|
# at this many windows so the seeded window is actually exercised in the preview.
|
|
37
37
|
_TUNE_WINDOW_COVERAGE = 3
|
|
38
|
+
# Hard ceiling on the loaded point count when seeded incidents pull the window
|
|
39
|
+
# back far into history — keeps a pathological (e.g. minute-grained, multi-year)
|
|
40
|
+
# series from bloating the page. Any incident still left older than this slice
|
|
41
|
+
# stays in the list but the client metrics ignore it (it cannot be scored).
|
|
42
|
+
_TUNE_INCIDENT_MAX_POINTS = 60_000
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
def default_window_points(seed_window: int) -> int:
|
|
@@ -77,6 +82,27 @@ def _normalize_seasonality_components(
|
|
|
77
82
|
return groups or None
|
|
78
83
|
|
|
79
84
|
|
|
85
|
+
def _earliest_incident_start(incidents: list[dict[str, str]]) -> datetime | None:
|
|
86
|
+
"""Earliest incident start (naive UTC) among seeded display dicts, or ``None``.
|
|
87
|
+
|
|
88
|
+
The display dicts carry ``"YYYY-MM-DD HH:MM:SS"`` naive-UTC strings (see
|
|
89
|
+
``autotune.labels.incidents_to_display``), matching the naive datetimes the
|
|
90
|
+
builder compares against the loaded series bounds.
|
|
91
|
+
"""
|
|
92
|
+
earliest: datetime | None = None
|
|
93
|
+
for inc in incidents:
|
|
94
|
+
raw = inc.get("start")
|
|
95
|
+
if not raw:
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
dt = datetime.strptime(str(raw), "%Y-%m-%d %H:%M:%S")
|
|
99
|
+
except ValueError:
|
|
100
|
+
continue
|
|
101
|
+
if earliest is None or dt < earliest:
|
|
102
|
+
earliest = dt
|
|
103
|
+
return earliest
|
|
104
|
+
|
|
105
|
+
|
|
80
106
|
def _seed_detector(metric_config: MetricConfig) -> dict[str, Any]:
|
|
81
107
|
"""Map the metric's current detector config to the TS ``DetectorParams`` shape.
|
|
82
108
|
|
|
@@ -146,6 +172,7 @@ def build_tune_payload(
|
|
|
146
172
|
project_name: str | None = None,
|
|
147
173
|
save_url: str | None = None,
|
|
148
174
|
incidents: list[dict[str, str]] | None = None,
|
|
175
|
+
capture_windows: list[dict[str, str]] | None = None,
|
|
149
176
|
) -> dict[str, Any]:
|
|
150
177
|
"""Assemble the interactive tuning payload from the persisted ``_dtk_datapoints``.
|
|
151
178
|
|
|
@@ -155,11 +182,16 @@ def build_tune_payload(
|
|
|
155
182
|
(``default_window_points``), not the whole history.
|
|
156
183
|
|
|
157
184
|
``incidents`` seeds the synced labeler with already-marked spans (display dicts
|
|
158
|
-
``{start, end, label}`` from ``incidents_to_display``).
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
``{start, end, label}`` from ``incidents_to_display``). When seeded incidents
|
|
186
|
+
fall outside the recent default slice, the loaded window is pulled back to
|
|
187
|
+
cover them so they render on the chart and count toward the live recall/FDR
|
|
188
|
+
metrics (an explicit ``start``/``--from`` is still honored verbatim).
|
|
189
|
+
``capture_windows`` seeds the threshold-capture regime scope (display dicts
|
|
190
|
+
``{start, end}``). ``labels_save_url`` (the POST endpoint for **Save
|
|
191
|
+
incidents**) is injected by the server, like ``save_url`` — it is ``None`` here.
|
|
161
192
|
"""
|
|
162
193
|
seed_incidents = incidents or []
|
|
194
|
+
seed_capture = capture_windows or []
|
|
163
195
|
name = metric_config.name
|
|
164
196
|
interval = metric_config.get_interval()
|
|
165
197
|
interval_seconds = interval.seconds
|
|
@@ -177,7 +209,30 @@ def build_tune_payload(
|
|
|
177
209
|
first = internal.get_first_datapoint_timestamp(name)
|
|
178
210
|
default_points = default_window_points(int(seed["windowSize"]))
|
|
179
211
|
lookback = end - timedelta(seconds=interval_seconds * default_points)
|
|
180
|
-
|
|
212
|
+
# Pull the window back to cover any seeded incidents so they actually
|
|
213
|
+
# render on the chart and can be scored — otherwise an incident older than
|
|
214
|
+
# the recent default slice shows in the list but never on the chart, and
|
|
215
|
+
# never counts toward recall/FDR. Give the earliest incident a few windows
|
|
216
|
+
# of leading context so the detector's trailing window can fill there.
|
|
217
|
+
earliest = _earliest_incident_start(seed_incidents)
|
|
218
|
+
if earliest is not None:
|
|
219
|
+
# The DB backend may return tz-aware timestamps (end/first) while
|
|
220
|
+
# incident display strings parse to naive UTC; align earliest to end's
|
|
221
|
+
# awareness (both represent UTC) so the comparison below is valid.
|
|
222
|
+
if end.tzinfo is not None and earliest.tzinfo is None:
|
|
223
|
+
earliest = earliest.replace(tzinfo=end.tzinfo)
|
|
224
|
+
elif end.tzinfo is None and earliest.tzinfo is not None:
|
|
225
|
+
earliest = earliest.replace(tzinfo=None)
|
|
226
|
+
context_points = max(_TUNE_WINDOW_COVERAGE * int(seed["windowSize"]), 200)
|
|
227
|
+
lookback = min(
|
|
228
|
+
lookback, earliest - timedelta(seconds=interval_seconds * context_points)
|
|
229
|
+
)
|
|
230
|
+
# Clamp to a hard point ceiling (a pathological history can't bloat the
|
|
231
|
+
# page) and to the first datapoint.
|
|
232
|
+
ceiling = end - timedelta(seconds=interval_seconds * _TUNE_INCIDENT_MAX_POINTS)
|
|
233
|
+
start = max(lookback, ceiling)
|
|
234
|
+
if first is not None:
|
|
235
|
+
start = max(start, first)
|
|
181
236
|
|
|
182
237
|
consecutive = 3
|
|
183
238
|
if metric_config.alerting:
|
|
@@ -198,6 +253,7 @@ def build_tune_payload(
|
|
|
198
253
|
"direction": direction,
|
|
199
254
|
"save_url": save_url,
|
|
200
255
|
"incidents": seed_incidents,
|
|
256
|
+
"capture_windows": seed_capture,
|
|
201
257
|
"labels_save_url": None,
|
|
202
258
|
}
|
|
203
259
|
if start is None or end is None:
|
|
@@ -238,5 +294,6 @@ def build_tune_payload(
|
|
|
238
294
|
"direction": direction,
|
|
239
295
|
"save_url": save_url,
|
|
240
296
|
"incidents": seed_incidents,
|
|
297
|
+
"capture_windows": seed_capture,
|
|
241
298
|
"labels_save_url": None,
|
|
242
299
|
}
|