detectkit 0.49.0__tar.gz → 0.49.2__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.49.0/detectkit.egg-info → detectkit-0.49.2}/PKG-INFO +1 -1
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/__init__.py +1 -1
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_decision.py +2 -1
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_replay.py +24 -7
- detectkit-0.49.2/detectkit/ui/assets/ui.js +255 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/overview.py +101 -6
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/server.py +62 -4
- {detectkit-0.49.0 → detectkit-0.49.2/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.49.0/detectkit/ui/assets/ui.js +0 -243
- {detectkit-0.49.0 → detectkit-0.49.2}/LICENSE +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/MANIFEST.in +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/README.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/result.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/_output.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/cli/main.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/profile.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/project_config.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/config/validator.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/interval.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/core/models.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/database/tables.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/html.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/tuning/server.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/html.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit/utils/stats.py +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/pyproject.toml +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/requirements.txt +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/setup.cfg +0 -0
- {detectkit-0.49.0 → detectkit-0.49.2}/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.49.
|
|
7
|
+
__version__ = "0.49.2"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -29,6 +29,7 @@ The multi-detector alert contract (documented in docs/guides/alerting.md):
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
31
|
import math
|
|
32
|
+
from collections.abc import Sequence
|
|
32
33
|
from datetime import datetime, timezone
|
|
33
34
|
|
|
34
35
|
import numpy as np
|
|
@@ -166,7 +167,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
166
167
|
def _count_consecutive_anomalies(
|
|
167
168
|
self,
|
|
168
169
|
detections_by_time: dict[np.datetime64, list[DetectionRecord]],
|
|
169
|
-
timestamps_sorted:
|
|
170
|
+
timestamps_sorted: Sequence[np.datetime64],
|
|
170
171
|
) -> tuple[int, list[DetectionRecord] | None, str | None]:
|
|
171
172
|
"""Walk timestamps newest→oldest counting quorum-satisfying points.
|
|
172
173
|
|
|
@@ -16,6 +16,8 @@ mutating ``_dtk_alert_states`` would be wrong.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
from collections import deque
|
|
20
|
+
from collections.abc import Sequence
|
|
19
21
|
from dataclasses import dataclass
|
|
20
22
|
from datetime import datetime, timedelta
|
|
21
23
|
|
|
@@ -76,7 +78,24 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
76
78
|
sim_last_recovery: np.datetime64 | None = None
|
|
77
79
|
events: list[ReplayedEvent] = []
|
|
78
80
|
|
|
81
|
+
# The causal view grows monotonically as the grid advances, so it is
|
|
82
|
+
# maintained incrementally: one sorted pass up front, then each grid
|
|
83
|
+
# step admits the newly-covered timestamps (appendleft keeps ts_desc
|
|
84
|
+
# newest-first at O(1)). Rebuilding the dict + re-sorting per grid
|
|
85
|
+
# point — the previous shape — is O(n^2 log n) and turns a month of a
|
|
86
|
+
# 1-minute metric into a multi-minute replay.
|
|
87
|
+
all_ts_asc = sorted(by_time)
|
|
88
|
+
causal: dict[np.datetime64, list[DetectionRecord]] = {}
|
|
89
|
+
ts_desc: deque[np.datetime64] = deque()
|
|
90
|
+
next_ts = 0
|
|
91
|
+
|
|
79
92
|
for t in self._replay_grid(start, end):
|
|
93
|
+
while next_ts < len(all_ts_asc) and all_ts_asc[next_ts] <= t:
|
|
94
|
+
ts = all_ts_asc[next_ts]
|
|
95
|
+
causal[ts] = by_time[ts]
|
|
96
|
+
ts_desc.appendleft(ts)
|
|
97
|
+
next_ts += 1
|
|
98
|
+
|
|
80
99
|
# No-data fires independently of the quorum (a single binary
|
|
81
100
|
# metric-level signal), only when configured and not in cooldown.
|
|
82
101
|
if (
|
|
@@ -92,9 +111,6 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
92
111
|
sim_last_alert = t
|
|
93
112
|
continue
|
|
94
113
|
|
|
95
|
-
causal = {ts: recs for ts, recs in by_time.items() if ts <= t}
|
|
96
|
-
ts_desc = sorted(causal, reverse=True)
|
|
97
|
-
|
|
98
114
|
consecutive, latest_quorum, direction = self._count_consecutive_anomalies(
|
|
99
115
|
causal, ts_desc
|
|
100
116
|
)
|
|
@@ -170,7 +186,7 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
170
186
|
def _replay_recovered(
|
|
171
187
|
self,
|
|
172
188
|
causal: dict[np.datetime64, list[DetectionRecord]],
|
|
173
|
-
ts_desc:
|
|
189
|
+
ts_desc: Sequence[np.datetime64],
|
|
174
190
|
sim_last_alert: np.datetime64,
|
|
175
191
|
) -> bool:
|
|
176
192
|
"""Pure half of :meth:`_RecoveryMixin._check_recovery_since_last_alert`.
|
|
@@ -184,8 +200,9 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
184
200
|
return True
|
|
185
201
|
|
|
186
202
|
# No fresh detections after the alert → assume recovery (mirrors the
|
|
187
|
-
# live "no fresh detections" branch).
|
|
188
|
-
|
|
203
|
+
# live "no fresh detections" branch). ts_desc is newest-first, so the
|
|
204
|
+
# head carries the maximum — no full scan needed.
|
|
205
|
+
if not ts_desc[0] > sim_last_alert:
|
|
189
206
|
return True
|
|
190
207
|
|
|
191
208
|
latest_ts = ts_desc[0]
|
|
@@ -242,7 +259,7 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
242
259
|
def _replay_streak(
|
|
243
260
|
self,
|
|
244
261
|
causal: dict[np.datetime64, list[DetectionRecord]],
|
|
245
|
-
ts_desc:
|
|
262
|
+
ts_desc: Sequence[np.datetime64],
|
|
246
263
|
) -> tuple[int, np.datetime64, bool]:
|
|
247
264
|
"""In-memory analog of :meth:`_DecisionMixin._resolve_streak`.
|
|
248
265
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";(()=>{var k=e=>String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function ke(e){let n=e/60;return e>=86400&&e%86400===0?e/86400+"d":e>=3600&&e%3600===0?e/3600+"h":n>=1&&e%60===0?n+"min":e+"s"}function F(e){return e==null||!Number.isFinite(e)?"\u2014":(e*100).toFixed(1)+"%"}function oe(e){return e==null||!Number.isFinite(e)?"\u2014":Math.round(e).toLocaleString("en-US")}function V(e){return e==null||!Number.isFinite(e)?"\u2014":`\u2248${e>=9.5?e.toFixed(0):e.toFixed(1)}/day`}function le(e,n){let r=Math.max(0,e-n),t=Math.round(r/6e4);if(t<1)return"just now";if(t<60)return`${t}m ago`;let o=Math.floor(t/60);if(o<24)return`${o}h ago`;let a=Math.floor(o/24);return a<30?`${a}d ago`:`${Math.floor(a/30)}mo ago`}function he(e,n){let r=Math.max(0,Math.round((n-e)/1e3));if(r<60)return`${r}s`;let t=Math.floor(r/60),o=r%60;if(t<60)return o?`${t}m ${o}s`:`${t}m`;let a=Math.floor(t/60),c=t%60;return`${a}h ${String(c).padStart(2,"0")}m`}function me(e){return new Date(e).toISOString().slice(0,19).replace("T"," ")}function de(e){let n=Math.round(e/60);if(n<60)return`${n}m`;let r=Math.floor(n/60),t=n%60;if(r<24)return r+"h"+(t?` ${t}m`:"");let o=Math.floor(r/24),a=r%24;return o+"d"+(a?` ${a}h`:"")}function xe(e,n){let r=new Map;for(let t of e){let o=n(t),a=r.get(o);a?a.push(t):r.set(o,[t])}return r}var Ye=new URLSearchParams(location.search).get("token")||"";function fe(e,n){let r=new URL(e,location.origin);if(r.searchParams.set("token",Ye),n)for(let[t,o]of Object.entries(n))r.searchParams.set(t,o);return r.toString()}function be(e,n){return fe(`/metric/${encodeURIComponent(e)}`,{window:n})}async function ye(e){let n=await e.text().catch(()=>"");return new Error(n||`HTTP ${e.status}`)}async function ge(e,n){let r=await fetch(fe(e,n));if(!r.ok)throw await ye(r);return r.json()}async function re(e,n){let r=await fetch(fe(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok)throw await ye(r);return r.json()}function we(e,n){return ge(`/api/stats/${encodeURIComponent(e)}`,{window:n})}function Te(){return ge("/api/jobs")}function Ce(e,n){return ge(`/api/job/${encodeURIComponent(e)}`,{offset:String(n)})}function Me(e){return re("/api/run",e)}function Se(e){return re("/api/autotune",e)}function Ee(e){return re("/api/unlock",e)}function Le(e){return re("/api/tune",e)}function $e(e){return re(`/api/job/${encodeURIComponent(e)}/stop`,{})}var Y="dtk-ui",Re=!1;function Ne(){if(Re)return;Re=!0;let e=`
|
|
2
|
+
.${Y}{
|
|
3
|
+
--clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;
|
|
4
|
+
--muted:#6e675b;--faint:#9a9384;
|
|
5
|
+
--term-bg:#211e1a;--term-border:#332f29;--term-text:#c9c2b4;
|
|
6
|
+
--st-anomaly:#d63232;--st-recovery:#36a64f;--st-nodata:#f0ad4e;--st-error:#5a7a8c;
|
|
7
|
+
--accent-green:#2e9e73;
|
|
8
|
+
--sans:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
9
|
+
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
10
|
+
/* dark-terminal local surfaces, derived from the term-* palette above */
|
|
11
|
+
--bg:var(--term-bg);--surface:#2a2620;--surface-2:#332e26;--border:var(--term-border);
|
|
12
|
+
--text:var(--term-text);--text-strong:var(--paper);
|
|
13
|
+
display:block;min-height:100vh;background:var(--bg);color:var(--text);
|
|
14
|
+
font-family:var(--sans);position:relative;
|
|
15
|
+
}
|
|
16
|
+
.${Y} *{box-sizing:border-box;}
|
|
17
|
+
.${Y} a{color:var(--clay);}
|
|
18
|
+
.${Y}-root{max-width:1400px;margin:0 auto;padding:16px 20px 56px;display:flex;
|
|
19
|
+
flex-direction:column;gap:16px;}
|
|
20
|
+
.dtk-ui-content{display:flex;flex-direction:column;gap:16px;}
|
|
21
|
+
|
|
22
|
+
/* --- header ------------------------------------------------------------- */
|
|
23
|
+
.dtk-ui-header{display:flex;align-items:center;justify-content:space-between;gap:14px;
|
|
24
|
+
flex-wrap:wrap;padding-bottom:2px;}
|
|
25
|
+
.dtk-ui-brand{display:flex;align-items:center;gap:9px;}
|
|
26
|
+
.dtk-ui-brand-dot{width:11px;height:11px;border-radius:3px;background:var(--clay);flex:0 0 auto;}
|
|
27
|
+
.dtk-ui-brand-name{font-family:var(--mono);font-size:14px;color:var(--text-strong);
|
|
28
|
+
letter-spacing:-0.01em;}
|
|
29
|
+
.dtk-ui-brand-name b{font-weight:700;}
|
|
30
|
+
.dtk-ui-header-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
|
31
|
+
.dtk-ui-seg{display:flex;gap:3px;background:var(--surface);border:1px solid var(--border);
|
|
32
|
+
border-radius:8px;padding:3px;}
|
|
33
|
+
.dtk-ui-seg-btn{border:0;background:transparent;color:var(--faint);font-family:var(--mono);
|
|
34
|
+
font-size:11.5px;padding:5px 10px;border-radius:6px;cursor:pointer;white-space:nowrap;}
|
|
35
|
+
.dtk-ui-seg-btn:hover{color:var(--text);}
|
|
36
|
+
.dtk-ui-seg-btn.on{background:var(--clay);color:#fff;font-weight:600;}
|
|
37
|
+
.dtk-ui-iconbtn{border:1px solid var(--border);background:var(--surface);color:var(--text);
|
|
38
|
+
border-radius:8px;width:32px;height:32px;display:inline-flex;align-items:center;
|
|
39
|
+
justify-content:center;cursor:pointer;font-size:14px;line-height:1;}
|
|
40
|
+
.dtk-ui-iconbtn:hover{border-color:var(--clay);color:#fff;background:var(--clay-700);}
|
|
41
|
+
.dtk-ui-iconbtn.spinning{animation:dtk-ui-spin 0.8s linear infinite;}
|
|
42
|
+
@keyframes dtk-ui-spin{to{transform:rotate(360deg);}}
|
|
43
|
+
.dtk-ui-runbtn{border:0;background:var(--clay);color:#fff;font-family:var(--sans);
|
|
44
|
+
font-size:13px;font-weight:600;padding:8px 15px;border-radius:8px;cursor:pointer;}
|
|
45
|
+
.dtk-ui-runbtn:hover{background:var(--clay-700);}
|
|
46
|
+
.dtk-ui-jobschip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--border);
|
|
47
|
+
background:var(--surface);color:var(--faint);font-family:var(--mono);font-size:11.5px;
|
|
48
|
+
padding:6px 12px;border-radius:999px;cursor:pointer;white-space:nowrap;}
|
|
49
|
+
.dtk-ui-jobschip:hover{border-color:var(--clay);}
|
|
50
|
+
.dtk-ui-jobschip-dot{width:7px;height:7px;border-radius:50%;background:var(--faint);flex:0 0 auto;}
|
|
51
|
+
.dtk-ui-jobschip.running .dtk-ui-jobschip-dot{background:var(--clay);
|
|
52
|
+
animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
|
|
53
|
+
.dtk-ui-jobschip.running{color:var(--text);border-color:var(--clay);}
|
|
54
|
+
@keyframes dtk-ui-pulse{0%,100%{opacity:1;}50%{opacity:0.35;}}
|
|
55
|
+
.dtk-ui-progresschip{font-family:var(--mono);font-size:11.5px;color:var(--faint);
|
|
56
|
+
white-space:nowrap;}
|
|
57
|
+
|
|
58
|
+
/* --- banner / empty ------------------------------------------------------ */
|
|
59
|
+
.dtk-ui-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
60
|
+
background:rgba(214,50,50,0.1);border:1px solid rgba(214,50,50,0.4);border-radius:10px;
|
|
61
|
+
padding:11px 14px;color:var(--text);font-size:13px;}
|
|
62
|
+
.dtk-ui-banner-retry{border:1px solid var(--st-anomaly);background:transparent;color:var(--st-anomaly);
|
|
63
|
+
border-radius:7px;padding:6px 13px;font-family:var(--sans);font-size:12.5px;cursor:pointer;
|
|
64
|
+
flex:0 0 auto;}
|
|
65
|
+
.dtk-ui-banner-retry:hover{background:rgba(214,50,50,0.15);}
|
|
66
|
+
.dtk-ui-empty{padding:60px 20px;text-align:center;color:var(--faint);font-size:14px;}
|
|
67
|
+
|
|
68
|
+
/* --- stat tiles ----------------------------------------------------------- */
|
|
69
|
+
.dtk-ui-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;}
|
|
70
|
+
.dtk-ui-tile{background:var(--surface);border:1px solid var(--border);border-radius:10px;
|
|
71
|
+
padding:12px 14px;display:flex;flex-direction:column;gap:3px;}
|
|
72
|
+
.dtk-ui-tile-val{font-family:var(--mono);font-size:23px;font-weight:700;color:var(--text-strong);
|
|
73
|
+
line-height:1.1;}
|
|
74
|
+
.dtk-ui-tile-label{font-family:var(--mono);font-size:10.5px;color:var(--faint);
|
|
75
|
+
text-transform:uppercase;letter-spacing:0.06em;}
|
|
76
|
+
.dtk-ui-tile-sub{font-size:11.5px;color:var(--muted);font-family:var(--mono);}
|
|
77
|
+
.dtk-ui-tile-sub.warn{color:var(--st-nodata);font-weight:600;}
|
|
78
|
+
.dtk-ui-tile.err .dtk-ui-tile-val{color:var(--st-anomaly);}
|
|
79
|
+
|
|
80
|
+
/* --- tag rollup strip ------------------------------------------------------ */
|
|
81
|
+
.dtk-ui-tags{display:flex;flex-wrap:wrap;gap:7px;}
|
|
82
|
+
.dtk-ui-tag{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;background:var(--surface);
|
|
83
|
+
border:1px solid var(--border);border-radius:999px;font-size:11.5px;color:var(--muted);
|
|
84
|
+
cursor:pointer;font-family:var(--mono);}
|
|
85
|
+
.dtk-ui-tag:hover{border-color:var(--clay);color:var(--text);}
|
|
86
|
+
.dtk-ui-tag.on{background:var(--clay);border-color:var(--clay);color:#fff;}
|
|
87
|
+
.dtk-ui-tag-name{font-weight:600;}
|
|
88
|
+
.dtk-ui-tag.on .dtk-ui-tag-name{color:#fff;}
|
|
89
|
+
.dtk-ui-tag-n{color:var(--text);}
|
|
90
|
+
.dtk-ui-tag.on .dtk-ui-tag-n,.dtk-ui-tag.on .dtk-ui-tag-sub{color:rgba(255,255,255,0.85);}
|
|
91
|
+
.dtk-ui-tag-sub{color:var(--faint);}
|
|
92
|
+
|
|
93
|
+
/* --- metrics table --------------------------------------------------------- */
|
|
94
|
+
.dtk-ui-table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
|
95
|
+
overflow:hidden;}
|
|
96
|
+
.dtk-ui-group + .dtk-ui-group{border-top:1px solid var(--border);}
|
|
97
|
+
.dtk-ui-group-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
|
|
98
|
+
padding:9px 14px;background:var(--surface-2);}
|
|
99
|
+
.dtk-ui-group-name{font-family:var(--mono);font-size:11.5px;color:var(--text);font-weight:600;}
|
|
100
|
+
.dtk-ui-group-sub{font-family:var(--mono);font-size:11px;color:var(--faint);}
|
|
101
|
+
.dtk-ui-table{width:100%;border-collapse:collapse;font-size:12.5px;}
|
|
102
|
+
.dtk-ui-table thead th{text-align:left;padding:8px 12px;font-family:var(--mono);font-size:10.5px;
|
|
103
|
+
color:var(--faint);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;
|
|
104
|
+
border-bottom:1px solid var(--border);white-space:nowrap;}
|
|
105
|
+
.dtk-ui-th{cursor:pointer;user-select:none;}
|
|
106
|
+
.dtk-ui-th:hover{color:var(--text);}
|
|
107
|
+
.dtk-ui-th-arrow{margin-left:5px;color:var(--clay);}
|
|
108
|
+
.dtk-ui-table td{padding:7px 12px;border-bottom:1px solid var(--border);vertical-align:middle;
|
|
109
|
+
color:var(--text);}
|
|
110
|
+
.dtk-ui-row:last-child td{border-bottom:0;}
|
|
111
|
+
.dtk-ui-row:hover{background:var(--surface-2);}
|
|
112
|
+
.dtk-ui-row.disabled{opacity:0.5;}
|
|
113
|
+
.dtk-ui-row.errored{background:rgba(214,50,50,0.05);}
|
|
114
|
+
.dtk-ui-row.pending{opacity:0.7;}
|
|
115
|
+
.dtk-ui-pending{font-family:var(--mono);color:var(--faint);}
|
|
116
|
+
.dtk-ui-dotcell{width:14px;}
|
|
117
|
+
.dtk-ui-dot{width:9px;height:9px;border-radius:50%;display:inline-block;cursor:help;}
|
|
118
|
+
.dtk-ui-namecell{min-width:170px;}
|
|
119
|
+
.dtk-ui-name{font-weight:600;color:var(--text-strong);}
|
|
120
|
+
.dtk-ui-err-badge{color:var(--st-anomaly);margin-left:6px;cursor:help;font-weight:700;}
|
|
121
|
+
.dtk-ui-tagchips{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px;}
|
|
122
|
+
.dtk-ui-tagchip{font-family:var(--mono);font-size:9.5px;color:var(--faint);
|
|
123
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;}
|
|
124
|
+
.dtk-ui-interval{font-family:var(--mono);color:var(--muted);white-space:nowrap;}
|
|
125
|
+
.dtk-ui-sparkcell{width:150px;}
|
|
126
|
+
.dtk-ui-spark-empty{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-style:italic;}
|
|
127
|
+
.dtk-ui-spark-loading{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-style:italic;
|
|
128
|
+
animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
|
|
129
|
+
.dtk-ui-alertscell{white-space:nowrap;}
|
|
130
|
+
.dtk-ui-alerts-n{font-family:var(--mono);color:var(--text);}
|
|
131
|
+
.dtk-ui-alerts-n.hasany{font-weight:700;color:var(--text-strong);}
|
|
132
|
+
.dtk-ui-alerts-n.overbudget{color:var(--st-anomaly);}
|
|
133
|
+
.dtk-ui-alerts-sub{font-family:var(--mono);font-size:10.5px;color:var(--faint);margin-left:5px;}
|
|
134
|
+
.dtk-ui-lastalert{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap;}
|
|
135
|
+
.dtk-ui-rate{font-family:var(--mono);color:var(--text);white-space:nowrap;}
|
|
136
|
+
.dtk-ui-quality{white-space:nowrap;cursor:help;}
|
|
137
|
+
.dtk-ui-quality-chip{font-family:var(--mono);font-size:11px;color:var(--muted);}
|
|
138
|
+
.dtk-ui-quality-chip b{color:var(--text);font-weight:600;}
|
|
139
|
+
.dtk-ui-quality.empty{color:var(--faint);}
|
|
140
|
+
.dtk-ui-lock{font-family:var(--mono);font-size:9px;letter-spacing:0.04em;color:var(--st-nodata);
|
|
141
|
+
border:1px solid var(--st-nodata);border-radius:4px;padding:1px 5px;cursor:help;}
|
|
142
|
+
.dtk-ui-actionscell{white-space:nowrap;text-align:right;}
|
|
143
|
+
.dtk-ui-actionbtn{border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
144
|
+
font-family:var(--sans);font-size:11px;padding:4px 9px;border-radius:6px;cursor:pointer;
|
|
145
|
+
margin-left:5px;}
|
|
146
|
+
.dtk-ui-actionbtn:hover{border-color:var(--clay);color:var(--text);}
|
|
147
|
+
|
|
148
|
+
/* --- detail overlay --------------------------------------------------------- */
|
|
149
|
+
.dtk-ui-overlay{position:fixed;inset:0;background:rgba(10,9,7,0.72);z-index:50;display:flex;
|
|
150
|
+
align-items:center;justify-content:center;padding:24px;}
|
|
151
|
+
.dtk-ui-overlay-modal{width:100%;height:100%;max-width:1500px;background:var(--surface);
|
|
152
|
+
border:1px solid var(--border);border-radius:12px;display:flex;flex-direction:column;
|
|
153
|
+
overflow:hidden;box-shadow:0 30px 80px -20px rgba(0,0,0,0.6);}
|
|
154
|
+
.dtk-ui-overlay-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
155
|
+
padding:11px 16px;border-bottom:1px solid var(--border);background:var(--surface-2);flex:0 0 auto;}
|
|
156
|
+
.dtk-ui-overlay-title{font-family:var(--mono);font-size:13.5px;color:var(--text-strong);
|
|
157
|
+
font-weight:700;}
|
|
158
|
+
.dtk-ui-overlay-sub{font-family:var(--mono);font-size:11px;color:var(--faint);margin-left:8px;}
|
|
159
|
+
.dtk-ui-overlay-actions{display:flex;align-items:center;gap:8px;}
|
|
160
|
+
.dtk-ui-overlay-close{border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
161
|
+
border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:14px;}
|
|
162
|
+
.dtk-ui-overlay-close:hover{border-color:var(--st-anomaly);color:var(--st-anomaly);}
|
|
163
|
+
.dtk-ui-overlay-body{flex:1;min-height:0;position:relative;background:var(--term-bg);}
|
|
164
|
+
.dtk-ui-overlay-body iframe{width:100%;height:100%;border:0;display:block;background:#fff;}
|
|
165
|
+
.dtk-ui-overlay-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
166
|
+
gap:10px;color:var(--term-text);font-family:var(--mono);font-size:13px;}
|
|
167
|
+
.dtk-ui-overlay-loading b{color:var(--clay);font-weight:600;}
|
|
168
|
+
.dtk-ui-overlay-spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--term-border);
|
|
169
|
+
border-top-color:var(--clay);animation:dtk-ui-spin 0.8s linear infinite;flex:none;}
|
|
170
|
+
@keyframes dtk-ui-spin{to{transform:rotate(360deg);}}
|
|
171
|
+
|
|
172
|
+
/* --- drawers (run panel + jobs) --------------------------------------------- */
|
|
173
|
+
.dtk-ui-drawer-backdrop{position:fixed;inset:0;background:rgba(10,9,7,0.5);z-index:40;display:none;}
|
|
174
|
+
.dtk-ui-drawer-backdrop.open{display:block;}
|
|
175
|
+
.dtk-ui-drawer{position:fixed;top:0;right:0;bottom:0;width:min(440px,100vw);background:var(--surface);
|
|
176
|
+
border-left:1px solid var(--border);z-index:41;display:none;flex-direction:column;
|
|
177
|
+
box-shadow:-20px 0 50px -20px rgba(0,0,0,0.6);}
|
|
178
|
+
.dtk-ui-drawer.open{display:flex;}
|
|
179
|
+
.dtk-ui-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
180
|
+
padding:13px 16px;border-bottom:1px solid var(--border);flex:0 0 auto;}
|
|
181
|
+
.dtk-ui-drawer-title{font-family:var(--mono);font-size:13px;color:var(--text-strong);
|
|
182
|
+
font-weight:700;text-transform:uppercase;letter-spacing:0.04em;}
|
|
183
|
+
.dtk-ui-drawer-close{border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
184
|
+
border-radius:7px;width:28px;height:28px;cursor:pointer;font-size:13px;}
|
|
185
|
+
.dtk-ui-drawer-close:hover{border-color:var(--st-anomaly);color:var(--st-anomaly);}
|
|
186
|
+
.dtk-ui-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:14px 16px;display:flex;
|
|
187
|
+
flex-direction:column;gap:14px;}
|
|
188
|
+
|
|
189
|
+
.dtk-ui-field{display:flex;flex-direction:column;gap:5px;}
|
|
190
|
+
.dtk-ui-field-label{font-family:var(--mono);font-size:10.5px;color:var(--faint);
|
|
191
|
+
text-transform:uppercase;letter-spacing:0.05em;}
|
|
192
|
+
.dtk-ui-input,.dtk-ui-select{background:var(--bg);color:var(--text);border:1px solid var(--border);
|
|
193
|
+
border-radius:7px;padding:8px 10px;font-family:var(--mono);font-size:12.5px;width:100%;}
|
|
194
|
+
.dtk-ui-input:focus,.dtk-ui-select:focus{outline:none;border-color:var(--clay);}
|
|
195
|
+
.dtk-ui-input::placeholder{color:var(--faint);}
|
|
196
|
+
.dtk-ui-row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
|
197
|
+
.dtk-ui-checks{display:flex;gap:14px;flex-wrap:wrap;}
|
|
198
|
+
.dtk-ui-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);
|
|
199
|
+
cursor:pointer;}
|
|
200
|
+
.dtk-ui-check input{accent-color:var(--clay);cursor:pointer;}
|
|
201
|
+
.dtk-ui-btnrow{display:flex;gap:8px;flex-wrap:wrap;}
|
|
202
|
+
.dtk-ui-btn{border:1px solid var(--border);background:var(--surface-2);color:var(--text);
|
|
203
|
+
font-family:var(--sans);font-size:13px;font-weight:600;padding:9px 15px;border-radius:8px;
|
|
204
|
+
cursor:pointer;flex:1 1 auto;}
|
|
205
|
+
.dtk-ui-btn:hover{border-color:var(--clay);}
|
|
206
|
+
.dtk-ui-btn:disabled{opacity:0.45;cursor:default;}
|
|
207
|
+
.dtk-ui-btn.primary{background:var(--clay);border-color:var(--clay);color:#fff;}
|
|
208
|
+
.dtk-ui-btn.primary:hover{background:var(--clay-700);}
|
|
209
|
+
.dtk-ui-btn.danger{background:transparent;border-color:var(--st-anomaly);color:var(--st-anomaly);}
|
|
210
|
+
.dtk-ui-btn.danger:hover{background:rgba(214,50,50,0.12);}
|
|
211
|
+
.dtk-ui-reason{font-size:11.5px;color:var(--st-nodata);}
|
|
212
|
+
.dtk-ui-log{background:var(--term-bg);border:1px solid var(--term-border);border-radius:9px;
|
|
213
|
+
padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--term-text);
|
|
214
|
+
max-height:320px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;}
|
|
215
|
+
.dtk-ui-log-empty{color:var(--faint);font-style:italic;}
|
|
216
|
+
.dtk-ui-log-line.exit-ok{color:var(--st-recovery);font-weight:700;}
|
|
217
|
+
.dtk-ui-log-line.exit-fail{color:var(--st-anomaly);font-weight:700;}
|
|
218
|
+
.dtk-ui-log-line.exit-stop{color:var(--st-nodata);font-weight:700;}
|
|
219
|
+
|
|
220
|
+
/* --- jobs drawer ------------------------------------------------------------ */
|
|
221
|
+
.dtk-ui-joblist{display:flex;flex-direction:column;gap:7px;}
|
|
222
|
+
.dtk-ui-jobrow{display:flex;flex-direction:column;gap:4px;background:var(--surface-2);
|
|
223
|
+
border:1px solid var(--border);border-radius:8px;padding:9px 11px;cursor:pointer;}
|
|
224
|
+
.dtk-ui-jobrow:hover{border-color:var(--clay);}
|
|
225
|
+
.dtk-ui-jobrow.active{border-color:var(--clay);box-shadow:inset 0 0 0 1px var(--clay);}
|
|
226
|
+
.dtk-ui-jobrow-top{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
227
|
+
.dtk-ui-jobrow-status{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);
|
|
228
|
+
font-size:10.5px;text-transform:uppercase;letter-spacing:0.04em;}
|
|
229
|
+
.dtk-ui-jobrow-dot{width:7px;height:7px;border-radius:50%;flex:0 0 auto;}
|
|
230
|
+
.dtk-ui-jobrow-dot.pulse{animation:dtk-ui-pulse 1.2s ease-in-out infinite;}
|
|
231
|
+
.dtk-ui-jobrow-label{font-family:var(--mono);font-size:12px;color:var(--text);word-break:break-word;}
|
|
232
|
+
.dtk-ui-jobrow-meta{font-family:var(--mono);font-size:10.5px;color:var(--faint);}
|
|
233
|
+
.dtk-ui-jobrow-actions{display:flex;gap:6px;margin-top:2px;}
|
|
234
|
+
.dtk-ui-joblink{font-family:var(--mono);font-size:11px;color:var(--clay);}
|
|
235
|
+
|
|
236
|
+
/* --- toasts ------------------------------------------------------------------ */
|
|
237
|
+
.dtk-toasts{position:fixed;right:18px;bottom:18px;z-index:60;display:flex;flex-direction:column;
|
|
238
|
+
gap:8px;max-width:360px;}
|
|
239
|
+
.dtk-toast{background:var(--surface);border:1px solid var(--border);border-radius:9px;
|
|
240
|
+
padding:11px 14px;font-family:var(--sans);font-size:13px;color:var(--text);
|
|
241
|
+
box-shadow:0 12px 30px -12px rgba(0,0,0,0.5);animation:dtk-ui-toast-in 0.18s ease-out;}
|
|
242
|
+
.dtk-toast-error{border-color:var(--st-anomaly);}
|
|
243
|
+
.dtk-toast-info{border-color:var(--clay);}
|
|
244
|
+
.dtk-toast-out{opacity:0;transform:translateY(6px);transition:opacity 0.2s,transform 0.2s;}
|
|
245
|
+
@keyframes dtk-ui-toast-in{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
|
|
246
|
+
|
|
247
|
+
/* --- responsive ------------------------------------------------------------- */
|
|
248
|
+
@media (max-width:860px){
|
|
249
|
+
.dtk-ui-drawer{width:100vw;}
|
|
250
|
+
.dtk-ui-row2{grid-template-columns:1fr;}
|
|
251
|
+
.dtk-ui-table-wrap{overflow-x:auto;}
|
|
252
|
+
}
|
|
253
|
+
`,n=document.createElement("style");n.setAttribute("data-dtk-ui",""),n.textContent=e,document.head.appendChild(n)}var K=null;function Ke(e){return K&&K.isConnected||(K=document.createElement("div"),K.className="dtk-toasts",e.appendChild(K)),K}function B(e,n,r){let t=Ke(e),o=document.createElement("div");o.className=`dtk-toast dtk-toast-${n}`,o.textContent=r,t.appendChild(o),window.setTimeout(()=>{o.classList.add("dtk-toast-out"),window.setTimeout(()=>o.remove(),220)},5e3)}function W(e,n,r,t){let o="dtk-ui-tile"+(t!=null&&t.err?" err":""),a=t!=null&&t.warn?"dtk-ui-tile-sub warn":"dtk-ui-tile-sub";return`<div class="${o}"><div class="dtk-ui-tile-val">${k(e)}</div><div class="dtk-ui-tile-label">${k(n)}</div>`+(r?`<div class="${a}">${k(r)}</div>`:"")+"</div>"}function Pe(e){let n=document.createElement("div");n.className="dtk-ui-tiles";let r=e.length,t=e.filter(s=>s.enabled).length,o=e.filter(s=>!s.pending),a=0,c=0,h=0,w=0,p=!1;for(let s of o)a+=s.alerts.anomaly,c+=s.alerts.no_data,s.alerts.anomaly>0&&h++,s.alerts.per_day!==null&&(w+=s.alerts.per_day,p=!0);let m=[];for(let s of o)if(s.enabled){if(s.last_point===null){m.push({m:s,lag:1/0});continue}s.lag_seconds!==null&&s.interval_seconds>0&&s.lag_seconds>2*s.interval_seconds&&m.push({m:s,lag:s.lag_seconds})}m.sort((s,g)=>g.lag-s.lag);let x=m.length===0?void 0:`worst: ${m[0].m.name}${Number.isFinite(m[0].lag)?` (${de(m[0].lag)})`:" (no data)"}`;n.innerHTML=W(`${t}/${r}`,"Metrics","enabled / total")+W(oe(a),"Alerts in window",p?V(w):void 0)+W(oe(c),"No-data events")+W(oe(h),"Metrics alerting")+W(oe(m.length),"Stale metrics",x,{warn:m.length>0,err:m.length>0});let b=o.filter(s=>s.quality!==null);if(b.length>0){let s=0,g=0,v=0,T=0,u=0;for(let S of b){let E=S.quality;E&&(s+=E.caught,g+=E.incidents_in_window,v+=E.false_alerts,T+=S.alerts.anomaly,S.budget>u&&(u=S.budget))}let L=g>0?s/g:null,l=T>0?v/T:null,f=l!==null&&u>0&&l>u;n.innerHTML+=W(F(L),"Labeled recall",`${b.length} metric(s) labeled`)+W(F(l),"False-alert rate",f?`\u25B2 over ${F(u)} budget`:void 0,{warn:f})}return n}var ae="untagged";function We(e){let n=new Map,r=(t,o)=>{let a=n.get(t);a||(a={tag:t,count:0,alerts:0,perDaySum:0,havePerDay:!1},n.set(t,a)),a.count++,a.alerts+=o.alerts.anomaly,o.alerts.per_day!==null&&(a.perDaySum+=o.alerts.per_day,a.havePerDay=!0)};for(let t of e)if(t.tags.length===0)r(ae,t);else for(let o of t.tags)r(o,t);return[...n.values()].sort((t,o)=>o.count-t.count||t.tag.localeCompare(o.tag))}function De(e,n,r){let t=document.createElement("div");t.className="dtk-ui-tags";let o=document.createElement("button");o.type="button",o.className="dtk-ui-tag"+(n===null?" on":""),o.innerHTML=`<span class="dtk-ui-tag-name">All</span><span class="dtk-ui-tag-n">${e.length}</span>`,o.onclick=()=>r(null),t.appendChild(o);for(let a of We(e)){let c=document.createElement("button");c.type="button",c.className="dtk-ui-tag"+(n===a.tag?" on":"");let h=a.havePerDay?` \xB7 ${V(a.perDaySum)}`:"";c.innerHTML=`<span class="dtk-ui-tag-name">${k(a.tag===ae?ae:`#${a.tag}`)}</span><span class="dtk-ui-tag-n">${a.count} metric${a.count===1?"":"s"}</span><span class="dtk-ui-tag-n">${a.alerts} alert${a.alerts===1?"":"s"}</span>`+(h?`<span class="dtk-ui-tag-sub">${k(h)}</span>`:""),c.onclick=()=>r(a.tag),t.appendChild(c)}return t}var Ge={"--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 ce(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||Ge[e]||"#888"}function Xe(e){let n=e.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let r=parseInt(n,16);return n.length!==6||Number.isNaN(r)?[209,91,54]:[r>>16&255,r>>8&255,r&255]}function _e(e,n){let[r,t,o]=Xe(e);return`rgba(${r},${t},${o},${n})`}var vt=Number.isFinite;var He=140,je=30;function Oe(e,n,r){let t=Math.max(1,window.devicePixelRatio||1);e.style.width=`${He}px`,e.style.height=`${je}px`,e.width=Math.round(He*t),e.height=Math.round(je*t);let o=e.getContext("2d");if(!o||(o.clearRect(0,0,e.width,e.height),n.length===0))return;let a=3*t,c=e.width,h=e.height,w=n[0].t,p=n[n.length-1].t,m=p-w||1,x=l=>a+(l-w)/m*Math.max(1,c-2*a),b=1/0,s=-1/0;for(let l of n)l.v!==null&&Number.isFinite(l.v)&&(l.v<b&&(b=l.v),l.v>s&&(s=l.v));let g=!Number.isFinite(b)||!Number.isFinite(s);g&&(b=0,s=1),s<=b&&(s=b+1);let v=l=>h-a-(l-b)/(s-b)*Math.max(1,h-2*a);if(g){let l=h/2;o.strokeStyle=_e(ce("--faint"),.5),o.lineWidth=1*t,o.setLineDash([2*t,2*t]),o.beginPath(),o.moveTo(a,l),o.lineTo(c-a,l),o.stroke(),o.setLineDash([]);return}o.strokeStyle=ce("--term-text"),o.lineWidth=1*t,o.lineJoin="round",o.beginPath();let T=!1;for(let l of n){if(l.v===null||!Number.isFinite(l.v)){T=!1;continue}let f=x(l.t),S=v(l.v);T?o.lineTo(f,S):(o.moveTo(f,S),T=!0)}if(o.stroke(),r.length===0)return;let u=[];for(let l of n)l.v!==null&&Number.isFinite(l.v)&&u.push([l.t,l.v]);let L=l=>{if(u.length===0)return null;if(l<=u[0][0])return u[0][1];if(l>=u[u.length-1][0])return u[u.length-1][1];for(let f=1;f<u.length;f++){let[S,E]=u[f];if(l<=S){let[R,P]=u[f-1],O=S===R?0:(l-R)/(S-R);return P+(E-P)*O}}return u[u.length-1][1]};o.fillStyle=ce("--st-anomaly");for(let l of r){if(l<w||l>p)continue;let f=L(l);f!==null&&(o.beginPath(),o.arc(x(l),v(f),2*t,0,Math.PI*2),o.fill())}}var ue='<span class="dtk-ui-pending">\xB7\xB7\xB7</span>';function pe(e){return{name:e.name,dir:e.dir,file:e.file,tags:e.tags,enabled:e.enabled,interval_seconds:e.interval_seconds,detectors:[],alert_rule:null,last_point:null,first_point_in_window:null,lag_seconds:null,locked:!1,points:0,flagged:0,anomaly_rate:null,alerts:{anomaly:0,recovery:0,no_data:0,per_day:null,last_ts:null},quality:null,budget:0,spark:[],spark_anoms:[],error:null,pending:!0}}var ve={alerts:"desc",name:"asc",rate:"desc",freshness:"desc"};function Je(e){var o;if(e.pending)return{color:"var(--faint)",title:"loading\u2026",rank:0};if(!e.enabled)return{color:"var(--faint)",title:"disabled",rank:-1};if(e.last_point===null)return{color:"var(--st-anomaly)",title:"no datapoints loaded yet",rank:1/0};let n=(o=e.lag_seconds)!=null?o:0,r=e.interval_seconds>0?n/e.interval_seconds:0,t=`lag ${de(Math.max(0,n))} (${r.toFixed(1)}\xD7 interval) \xB7 last point ${me(e.last_point)} UTC`;return r<2?{color:"var(--st-recovery)",title:t,rank:n}:r<6?{color:"var(--st-nodata)",title:t,rank:n}:{color:"var(--st-anomaly)",title:t,rank:n}}function Qe(e){return e.length===0?"":`<div class="dtk-ui-tagchips">${e.map(n=>`<span class="dtk-ui-tagchip">${k(n)}</span>`).join("")}</div>`}function Ve(e){let n=e.quality;if(!n)return'<span class="dtk-ui-quality empty">\u2014</span>';let r=`Incidents: ${n.incidents} (${n.incidents_in_window} in window) \xB7 caught ${n.caught} \xB7 false alerts ${n.false_alerts} \xB7 reviewed ${n.reviewed} (valid ${n.reviewed_valid}, false ${n.reviewed_false}) \xB7 ${n.labels_file}`;return`<span class="dtk-ui-quality" title="${k(r)}"><span class="dtk-ui-quality-chip">R <b>${k(F(n.recall))}</b></span> \xB7 <span class="dtk-ui-quality-chip">FDR <b>${k(F(n.fdr))}</b></span> \xB7 <span class="dtk-ui-quality-chip">\u2713${n.reviewed_valid}</span></span>`}function Ze(e){let n=e.alert_rule?`min_detectors=${e.alert_rule.min_detectors} \xB7 direction=${e.alert_rule.direction} \xB7 consecutive=${e.alert_rule.consecutive} (${e.alert_rule.enabled}/${e.alert_rule.configs} config(s) enabled)`:"no alerting configured";return`detectors: ${e.detectors.join(", ")||"\u2014"}
|
|
254
|
+
alert rule: ${n}
|
|
255
|
+
file: ${e.file}`}function et(e,n,r,t){let o=document.createElement("tr");o.className="dtk-ui-row"+(e.enabled?"":" disabled")+(e.error?" errored":"")+(e.pending?" pending":"");let a=Je(e),c=document.createElement("td");c.className="dtk-ui-dotcell",c.innerHTML=`<span class="dtk-ui-dot" style="background:${a.color}" title="${k(a.title)}"></span>`,o.appendChild(c);let h=document.createElement("td");h.className="dtk-ui-namecell";let w=e.error?`<span class="dtk-ui-err-badge" title="${k(e.error)}">!</span>`:"";h.title=Ze(e),h.innerHTML=`<span class="dtk-ui-name">${k(e.name)}</span>${w}${Qe(e.tags)}`,o.appendChild(h);let p=document.createElement("td");p.innerHTML=`<span class="dtk-ui-interval">${k(ke(e.interval_seconds))}</span>`,o.appendChild(p);let m=document.createElement("td");if(m.className="dtk-ui-sparkcell",e.pending)m.innerHTML='<span class="dtk-ui-spark-loading">loading\u2026</span>';else if(e.spark.length===0)m.innerHTML='<span class="dtk-ui-spark-empty">no data yet</span>';else{let f=document.createElement("canvas");f.className="dtk-spark",m.appendChild(f),t.push({canvas:f,points:e.spark.map(([S,E])=>({t:S,v:E})),anoms:e.spark_anoms})}o.appendChild(m);let x=document.createElement("td");if(x.className="dtk-ui-alertscell",e.pending)x.innerHTML=ue;else{let f=e.quality!==null&&e.quality.fdr!==null&&e.quality.fdr>e.budget,S="dtk-ui-alerts-n"+(e.alerts.anomaly>0?" hasany":"")+(f?" overbudget":""),E=e.alerts.per_day!==null?`<span class="dtk-ui-alerts-sub">\xB7 ${k(V(e.alerts.per_day))}</span>`:"";x.innerHTML=`<span class="${S}">${e.alerts.anomaly}</span>${E}`}o.appendChild(x);let b=document.createElement("td");e.pending?b.innerHTML=ue:e.alerts.last_ts!==null?b.innerHTML=`<span class="dtk-ui-lastalert" title="${k(me(e.alerts.last_ts))} UTC">${k(le(n,e.alerts.last_ts))}</span>`:b.innerHTML='<span class="dtk-ui-lastalert">\u2014</span>',o.appendChild(b);let s=document.createElement("td");s.innerHTML=e.pending?ue:`<span class="dtk-ui-rate">${k(F(e.anomaly_rate))}</span>`,o.appendChild(s);let g=document.createElement("td");g.innerHTML=e.pending?ue:Ve(e),o.appendChild(g);let v=document.createElement("td");v.innerHTML=e.locked?'<span class="dtk-ui-lock" title="pipeline lock currently held for this metric">LOCK</span>':"",o.appendChild(v);let T=document.createElement("td");T.className="dtk-ui-actionscell";let u=document.createElement("button");u.type="button",u.className="dtk-ui-actionbtn",u.textContent="Open",u.onclick=()=>r.onOpen(e.name);let L=document.createElement("button");L.type="button",L.className="dtk-ui-actionbtn",L.textContent="Tune",L.onclick=()=>r.onTune(e.name);let l=document.createElement("button");return l.type="button",l.className="dtk-ui-actionbtn",l.textContent="Run",l.onclick=()=>r.onRun(e.name),T.append(u,L,l),o.appendChild(T),o}function Ae(e,n){var r;switch(n){case"alerts":return e.alerts.anomaly;case"name":return e.name.toLowerCase();case"rate":return(r=e.anomaly_rate)!=null?r:-1;case"freshness":return Je(e).rank}}function tt(e,n){let r=e.filter(a=>a.enabled),t=e.filter(a=>!a.enabled),o=n.dir==="asc"?1:-1;return r.sort((a,c)=>{let h=Ae(a,n.key),w=Ae(c,n.key);return h<w?-1*o:h>w?1*o:a.name.localeCompare(c.name)}),t.sort((a,c)=>a.name.localeCompare(c.name)),[...r,...t]}var nt=[{label:"\u25CF",key:"freshness"},{label:"Name",key:"name"},{label:"Interval",key:null},{label:"Trend",key:null},{label:"Alerts",key:"alerts"},{label:"Last alert",key:null},{label:"Rate",key:"rate"},{label:"Quality",key:null},{label:"",key:null},{label:"",key:null}];function ot(e,n){let r=document.createElement("tr");for(let t of nt){let o=document.createElement("th");if(t.key){o.className="dtk-ui-th";let a=e.key===t.key?`<span class="dtk-ui-th-arrow">${e.dir==="asc"?"\u25B5":"\u25BE"}</span>`:"";o.innerHTML=`${k(t.label)}${a}`,o.onclick=()=>n.onSortChange(t.key)}else o.textContent=t.label;r.appendChild(o)}return r}function rt(e){return e===""?"metrics/":`metrics/${e}/`}function Be(e,n,r,t){var w;let o=[],a=document.createElement("div");if(a.className="dtk-ui-table-wrap",e.length===0)return a.innerHTML='<div class="dtk-ui-empty">No metrics match the current filter.</div>',{el:a,paint:()=>{}};let c=xe(e,p=>p.dir),h=[...c.keys()].sort((p,m)=>p===m?0:p===""?-1:m===""?1:p.localeCompare(m));for(let p of h){let m=(w=c.get(p))!=null?w:[],x=document.createElement("div");x.className="dtk-ui-group";let b=m.reduce((u,L)=>u+L.alerts.anomaly,0),s=document.createElement("div");s.className="dtk-ui-group-head",s.innerHTML=`<span class="dtk-ui-group-name">${k(rt(p))}</span><span class="dtk-ui-group-sub">${m.length} metric${m.length===1?"":"s"} \xB7 ${b} alert${b===1?"":"s"}</span>`,x.appendChild(s);let g=document.createElement("table");g.className="dtk-ui-table";let v=document.createElement("thead");v.appendChild(ot(n,t)),g.appendChild(v);let T=document.createElement("tbody");for(let u of tt(m,n))T.appendChild(et(u,r,t,o));g.appendChild(T),x.appendChild(g),a.appendChild(x)}return{el:a,paint:()=>{for(let p of o)Oe(p.canvas,p.points,p.anoms)}}}function Ie(e,n,r,t){let o=document.createElement("div");o.className="dtk-ui-overlay";let a=document.createElement("div");a.className="dtk-ui-overlay-modal",o.appendChild(a);let c=document.createElement("div");c.className="dtk-ui-overlay-head",c.innerHTML=`<span><span class="dtk-ui-overlay-title">${k(n)}</span><span class="dtk-ui-overlay-sub">window: ${k(r)}</span></span>`;let h=document.createElement("div");h.className="dtk-ui-overlay-actions";let w=document.createElement("button");w.type="button",w.className="dtk-ui-btn",w.textContent="Tune",w.onclick=()=>t.onTune(n);let p=document.createElement("button");p.type="button",p.className="dtk-ui-overlay-close",p.textContent="\u2715",p.title="Close (Esc)",h.append(w,p),c.appendChild(h),a.appendChild(c);let m=document.createElement("div");m.className="dtk-ui-overlay-body";let x=document.createElement("div");x.className="dtk-ui-overlay-loading",x.innerHTML=`<span class="dtk-ui-overlay-spinner"></span><span>Building the report for <b>${k(n)}</b>\u2026</span>`,m.appendChild(x);let b=document.createElement("iframe");b.title=`detectkit report \u2014 ${n}`,b.style.visibility="hidden",b.addEventListener("load",()=>{x.style.display="none",b.style.visibility="visible"}),b.src=be(n,r),m.appendChild(b),a.appendChild(m),e.appendChild(o);function s(){document.removeEventListener("keydown",g),o.remove(),t.onClose()}function g(v){v.key==="Escape"&&s()}return o.addEventListener("click",v=>{v.target===o&&s()}),p.onclick=s,document.addEventListener("keydown",g),{setWindow(v){let T=c.querySelector(".dtk-ui-overlay-sub");T&&(T.textContent=`window: ${v}`),x.style.display="",b.style.visibility="hidden",b.src=be(n,v)},close:s}}var ze=/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/,Fe="dtk-ui-run-select-options";function qe(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Run pipeline</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let h=document.createElement("div");h.className="dtk-ui-field";let w=document.createElement("datalist");w.id=Fe;let p=document.createElement("input");p.type="text",p.className="dtk-ui-input",p.placeholder="metric name, tag:x, glob, or *",p.value="*",p.setAttribute("list",Fe),h.innerHTML='<span class="dtk-ui-field-label">Select</span>',h.append(p,w),c.appendChild(h);let m=document.createElement("div");m.className="dtk-ui-field",m.innerHTML='<span class="dtk-ui-field-label">Steps</span>';let x=document.createElement("div");x.className="dtk-ui-checks";let b={};for(let C of["load","detect","alert"]){let $=document.createElement("label");$.className="dtk-ui-check";let N=document.createElement("input");N.type="checkbox",N.checked=!0,N.onchange=H,$.append(N,document.createTextNode(C)),x.appendChild($),b[C]=N}m.appendChild(x),c.appendChild(m);let s=document.createElement("div");s.className="dtk-ui-row2";let g=document.createElement("div");g.className="dtk-ui-field",g.innerHTML='<span class="dtk-ui-field-label">From</span>';let v=document.createElement("input");v.type="text",v.className="dtk-ui-input",v.placeholder="YYYY-MM-DD [HH:MM:SS]",v.oninput=H,g.appendChild(v);let T=document.createElement("div");T.className="dtk-ui-field",T.innerHTML='<span class="dtk-ui-field-label">To</span>';let u=document.createElement("input");u.type="text",u.className="dtk-ui-input",u.placeholder="YYYY-MM-DD [HH:MM:SS]",u.oninput=H,T.appendChild(u),s.append(g,T),c.appendChild(s);let L=document.createElement("div");L.className="dtk-ui-checks";let l=document.createElement("label");l.className="dtk-ui-check";let f=document.createElement("input");f.type="checkbox",l.append(f,document.createTextNode("force (skip lock check)"));let S=document.createElement("label");S.className="dtk-ui-check";let E=document.createElement("input");E.type="checkbox",S.append(E,document.createTextNode("full refresh")),L.append(l,S),c.appendChild(L);let R=document.createElement("div");R.className="dtk-ui-btnrow";let P=document.createElement("button");P.type="button",P.className="dtk-ui-btn primary",P.textContent="Run";let O=document.createElement("button");O.type="button",O.className="dtk-ui-btn",O.textContent="Autotune";let _=document.createElement("button");_.type="button",_.className="dtk-ui-btn danger",_.textContent="Unlock",R.append(P,O,_),c.appendChild(R);let D=document.createElement("div");D.className="dtk-ui-reason",c.appendChild(D);let I=document.createElement("div");I.className="dtk-ui-field",I.innerHTML='<span class="dtk-ui-field-label">Log</span>';let A=document.createElement("div");A.className="dtk-ui-log";let Z=document.createElement("div");Z.className="dtk-ui-log-body";let z=document.createElement("div");z.className="dtk-ui-log-line",A.append(Z,z),I.appendChild(A),c.appendChild(I);let q=[];function ee(){Z.innerHTML=q.length===0?'<span class="dtk-ui-log-empty">no output yet</span>':q.map(k).join("<br>")}ee();function G(){return A.scrollTop+A.clientHeight>=A.scrollHeight-24}function te(){return Object.keys(b).filter(C=>b[C].checked)}function H(){let C=n.isPipelineBusy(),$=p.value.trim(),N=v.value.trim()===""||ze.test(v.value.trim()),i=u.value.trim()===""||ze.test(u.value.trim()),d="";C.busy?d=C.reason:$===""?d="select is required":!N||!i?d="from/to must be YYYY-MM-DD or YYYY-MM-DD HH:MM:SS":te().length===0&&(d="pick at least one step to run"),D.textContent=d,P.disabled=d!=="",O.disabled=C.busy||$===""||!N||!i,_.disabled=C.busy||$===""}function ie(){return{select:p.value.trim(),steps:te(),from:v.value.trim()||null,to:u.value.trim()||null,full_refresh:E.checked,force:f.checked}}P.onclick=()=>n.submitRun(ie()),O.onclick=()=>n.submitAutotune({select:p.value.trim(),from:v.value.trim()||null,to:u.value.trim()||null}),_.onclick=()=>{let C=p.value.trim();window.confirm(`Unlock the pipeline lock for "${C}"? Only do this if you're sure no dtk process is actually running against it.`)&&n.submitUnlock({select:C})};function ne(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=ne,r.onclick=ne,e.append(r,t),{el:t,open(C){C&&(p.value=C),r.classList.add("open"),t.classList.add("open"),H()},close:ne,isOpen(){return t.classList.contains("open")},refreshOptions(){w.innerHTML=["*",...n.getSelectOptions()].map(C=>`<option value="${k(C)}"></option>`).join("")},refreshBusyState:H,resetLog(){q=[],ee(),z.textContent="",z.className="dtk-ui-log-line"},appendLog(C){if(C.length===0)return;let $=G();q.push(...C),ee(),$&&(A.scrollTop=A.scrollHeight)},setLogStatus(C,$){if(C==="running")return;let N=C==="done"&&$===0?"exit-ok":C==="stopped"?"exit-stop":"exit-fail";z.className=`dtk-ui-log-line ${N}`,z.textContent=`\u2500\u2500 ${C} (exit ${$!=null?$:"?"}) \u2500\u2500`}}}function at(e){return e==="done"?"var(--st-recovery)":e==="failed"?"var(--st-anomaly)":e==="running"?"var(--clay)":"var(--faint)"}function Ue(e,n){let r=document.createElement("div");r.className="dtk-ui-drawer-backdrop";let t=document.createElement("div");t.className="dtk-ui-drawer";let o=document.createElement("div");o.className="dtk-ui-drawer-head",o.innerHTML='<span class="dtk-ui-drawer-title">Jobs</span>';let a=document.createElement("button");a.type="button",a.className="dtk-ui-drawer-close",a.textContent="\u2715",o.appendChild(a),t.appendChild(o);let c=document.createElement("div");c.className="dtk-ui-drawer-body",t.appendChild(c);let h=document.createElement("div");h.className="dtk-ui-joblist",c.appendChild(h);function w(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=w,r.onclick=w,e.append(r,t),{el:t,open(){r.classList.add("open"),t.classList.add("open")},close:w,isOpen(){return t.classList.contains("open")},render(p,m,x){var b;if(p.length===0){h.innerHTML='<div class="dtk-ui-empty">No jobs yet.</div>';return}h.innerHTML="";for(let s of p){let g=document.createElement("div");g.className="dtk-ui-jobrow"+(s.id===x?" active":"");let v=s.status==="running"?" pulse":"",T=he(s.started_at,(b=s.finished_at)!=null?b:m),u=document.createElement("div");u.className="dtk-ui-jobrow-top",u.innerHTML=`<span class="dtk-ui-jobrow-status"><span class="dtk-ui-jobrow-dot${v}" style="background:${at(s.status)}"></span>${k(s.kind)} \xB7 ${k(s.status)}</span><span class="dtk-ui-jobrow-meta">${k(le(m,s.started_at))} \xB7 ${k(T)}</span>`,g.appendChild(u);let L=document.createElement("div");L.className="dtk-ui-jobrow-label",L.textContent=s.label,g.appendChild(L);let l=document.createElement("div");if(l.className="dtk-ui-jobrow-actions",s.status==="running"){let f=document.createElement("button");f.type="button",f.className="dtk-ui-actionbtn",f.textContent="Stop",f.onclick=S=>{S.stopPropagation(),n.onStop(s.id)},l.appendChild(f)}if(s.kind==="tune"&&s.url){let f=document.createElement("a");f.className="dtk-ui-joblink",f.href=s.url,f.target="_blank",f.rel="noopener",f.textContent="Open tuner",f.onclick=S=>S.stopPropagation(),l.appendChild(f)}l.childElementCount>0&&g.appendChild(l),g.onclick=()=>n.onFollow(s.id),h.appendChild(g)}}}}var it=3,st=[{value:"24h",label:"24h"},{value:"7d",label:"7d"},{value:"30d",label:"30d"},{value:"90d",label:"90d"},{value:"all",label:"All"}];function lt(e,n){Ne(),n.classList.add(Y),n.innerHTML="";let r=document.createElement("div");r.className=`${Y}-root`,n.appendChild(r);let t={windowPreset:e.initial_window||"30d",metrics:e.metrics.map(pe),jobs:[],followedJobId:null,followOffset:0,tagFilter:null,sort:{key:"alerts",dir:ve.alerts}},o=null,a,c;function h(){let i=new Set,d=new Set;for(let M of e.metrics){i.add(M.name);for(let j of M.tags)d.add(j)}return[...i,...[...d].map(M=>`tag:${M}`)]}function w(){let i=t.jobs.find(d=>d.status==="running"&&d.kind!=="tune");return i?{busy:!0,reason:`a pipeline job is already running (${i.label})`}:{busy:!1,reason:""}}let p=document.createElement("div");p.className="dtk-ui-header",r.appendChild(p);let m=document.createElement("div");m.className="dtk-ui-brand",m.innerHTML=`<span class="dtk-ui-brand-dot"></span><span class="dtk-ui-brand-name">detectkit \xB7 <b>${k(e.project)}</b></span>`,p.appendChild(m);let x=document.createElement("div");x.className="dtk-ui-header-right",p.appendChild(x);let b=document.createElement("div");b.className="dtk-ui-seg";for(let i of st){let d=document.createElement("button");d.type="button",d.className="dtk-ui-seg-btn"+(t.windowPreset===i.value?" on":""),d.textContent=i.label,d.onclick=()=>{t.windowPreset!==i.value&&(t.windowPreset=i.value,b.querySelectorAll(".dtk-ui-seg-btn").forEach(M=>M.classList.remove("on")),d.classList.add("on"),o&&o.setWindow(i.value),N())},b.appendChild(d)}x.appendChild(b);let s=document.createElement("button");s.type="button",s.className="dtk-ui-iconbtn",s.title="Refresh overview",s.textContent="\u27F3",s.onclick=()=>{N()},x.appendChild(s);let g=document.createElement("button");g.type="button",g.className="dtk-ui-runbtn",g.textContent="Run pipeline",g.onclick=()=>P(),x.appendChild(g);let v=document.createElement("span");v.className="dtk-ui-progresschip",v.style.display="none",x.appendChild(v);function T(i,d){if(d===0||i>=d){v.style.display="none";return}v.textContent=`${i}/${d}`,v.style.display=""}let u=document.createElement("button");u.type="button",u.className="dtk-ui-jobschip",u.innerHTML='<span class="dtk-ui-jobschip-dot"></span><span>idle</span>',u.onclick=()=>{R.isOpen()?R.close():O()},x.appendChild(u);function L(){let i=t.jobs.find(j=>j.status==="running");u.classList.toggle("running",!!i);let d=i?`${i.kind} ${i.label}`:"idle",M=u.querySelector("span:last-child");M&&(M.textContent=d),u.title=i?`Started ${new Date(i.started_at).toLocaleString()}`:"No jobs running"}let l=document.createElement("div");l.className="dtk-ui-content",r.appendChild(l);function f(){var Q,U;l.innerHTML="";let i=e.metrics.length;if(i===0){let y=document.createElement("div");y.className="dtk-ui-empty",y.textContent="No metrics found for this project/selector.",l.appendChild(y);return}let d=t.metrics.filter(y=>!y.pending),M=d.filter(y=>y.error!==null);if(d.length===i&&M.length===i){let y=document.createElement("div");y.className="dtk-ui-banner";let se=(U=(Q=M[0])==null?void 0:Q.error)!=null?U:"unknown error";y.innerHTML=`<span>Failed to load overview: every metric failed (${k(se)}).</span>`;let J=document.createElement("button");J.type="button",J.className="dtk-ui-banner-retry",J.textContent="Retry",J.onclick=()=>{N()},y.appendChild(J),l.appendChild(y)}l.appendChild(Pe(t.metrics)),l.appendChild(De(d,t.tagFilter,y=>{t.tagFilter=y,f()}));let j=t.tagFilter===null?t.metrics:t.metrics.filter(y=>t.tagFilter===ae?y.tags.length===0:y.tags.includes(t.tagFilter)),X=Be(j,t.sort,Date.now(),{onOpen:S,onTune:y=>{q(y)},onRun:y=>P(y),onSortChange:y=>{t.sort=t.sort.key===y?{key:y,dir:t.sort.dir==="asc"?"desc":"asc"}:{key:y,dir:ve[y]},f()}});l.appendChild(X.el),X.paint()}function S(i){o&&o.close(),o=Ie(r,i,t.windowPreset,{onTune:d=>{q(d)},onClose:()=>{o=null}})}let E=qe(r,{submitRun:i=>{A(i)},submitAutotune:i=>{Z(i)},submitUnlock:i=>{z(i)},getSelectOptions:h,isPipelineBusy:w}),R=Ue(r,{onFollow:i=>G(i),onStop:i=>{ee(i)}});function P(i){R.close(),E.refreshOptions(),E.refreshBusyState(),E.open(i)}function O(){E.close(),R.render(t.jobs,Date.now(),t.followedJobId),R.open(),ie()}function _(i,d,M,j){let X={id:M,kind:i,label:d,status:"running",returncode:null,url:j,started_at:Date.now(),finished_at:null};t.jobs=[X,...t.jobs.filter(Q=>Q.id!==M)],te(),ie(),H()}let D=!1,I=new Set;async function A(i){if(!D){D=!0;try{let d=await Me(i);_("run",`run --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function Z(i){if(!D){D=!0;try{let d=await Se(i);_("autotune",`autotune --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function z(i){if(!D){D=!0;try{let d=await Ee(i);_("unlock",`unlock --select ${i.select}`,d.job_id,null),G(d.job_id)}catch(d){B(r,"error",d.message)}finally{D=!1}}}async function q(i){if(!I.has(i)){I.add(i),B(r,"info",`Opening tuner for ${i}\u2026`);try{let d=await Le({metric:i});_("tune",`tune --select ${i}`,d.job_id,d.url),window.open(d.url,"_blank")}catch(d){B(r,"error",d.message)}finally{I.delete(i)}}}async function ee(i){try{await $e(i),B(r,"info","Stop requested."),H()}catch(d){B(r,"error",d.message)}}function G(i){t.followedJobId=i,t.followOffset=0,P(),E.resetLog(),C(i)}function te(){L(),R.render(t.jobs,Date.now(),t.followedJobId),E.refreshBusyState()}async function H(){try{let i=await Te();t.jobs=i.jobs}catch{}te()}function ie(){if(a!==void 0)return;let i=()=>{H().then(()=>{a=R.isOpen()||t.jobs.some(M=>M.status==="running")?window.setTimeout(i,2e3):void 0})};a=window.setTimeout(i,2e3)}function ne(){c!==void 0&&(window.clearTimeout(c),c=void 0)}function C(i){ne();let d=()=>{Ce(i,t.followOffset).then(M=>{if(t.followedJobId===i){if(E.appendLog(M.lines),t.followOffset=M.next_offset,M.status!=="running"){E.setLogStatus(M.status,M.returncode),c=void 0,H();return}c=window.setTimeout(d,1e3)}}).catch(M=>{B(r,"error",`job ${i}: ${M.message}`),c=void 0})};c=window.setTimeout(d,0)}let $=0;async function N(){let i=++$,d=t.windowPreset;if(t.metrics=e.metrics.map(pe),s.classList.add("spinning"),T(0,e.metrics.length),f(),e.metrics.length===0){s.classList.remove("spinning"),T(0,0);return}let M=[...e.metrics],j=0;async function X(){for(;;){if(i!==$)return;let U=M.shift();if(!U)return;let y;try{y=await we(U.name,d),y.pending=!1}catch(J){y={...pe(U),pending:!1,error:J.message}}if(i!==$)return;let se=t.metrics.findIndex(J=>J.name===U.name);se!==-1&&(t.metrics[se]=y),j++,T(j,e.metrics.length),f()}}let Q=Math.min(it,e.metrics.length);await Promise.all(Array.from({length:Q},()=>X())),i===$&&(s.classList.remove("spinning"),T(j,e.metrics.length),E.refreshOptions())}f(),E.refreshOptions(),N(),H()}window.__DTK_UI__={render:lt};})();
|
|
@@ -27,6 +27,7 @@ from detectkit.autotune.labels import IncidentLabels, newest_labels_file, parse_
|
|
|
27
27
|
from detectkit.config.metric_config import AlertConfig, MetricConfig
|
|
28
28
|
from detectkit.config.project_config import ProjectConfig
|
|
29
29
|
from detectkit.database.internal_tables import InternalTablesManager
|
|
30
|
+
from detectkit.detectors.factory import DetectorFactory
|
|
30
31
|
from detectkit.reporting.builder import _ms, _num_or_none, record_from_row, replay_alert_events
|
|
31
32
|
from detectkit.tuning.payload import DEFAULT_FALSE_ALERT_BUDGET
|
|
32
33
|
from detectkit.utils.datetime_utils import now_utc_naive
|
|
@@ -95,6 +96,60 @@ def _alert_rule_summary(alerting: list[AlertConfig] | None) -> dict[str, Any] |
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def _configured_detector_ids(config: MetricConfig) -> list[str]:
|
|
100
|
+
"""The detector ids the metric's CURRENT config would run under.
|
|
101
|
+
|
|
102
|
+
Mirrors the detect step's id derivation (``get_algorithm_params`` +
|
|
103
|
+
seasonality → ``DetectorFactory.create_from_config`` → ``get_detector_id``).
|
|
104
|
+
Every retune/autotune changes a detector's id, and the superseded ids'
|
|
105
|
+
rows stay in ``_dtk_detections`` forever — so an unfiltered window read
|
|
106
|
+
returns one row-set per historical config generation: N× the volume, and
|
|
107
|
+
replayed quorums that mix live and dead configs (inflating alert counts
|
|
108
|
+
a real pipeline run would never produce). The overview answers "how does
|
|
109
|
+
the metric behave under its current config", so it reads only these ids.
|
|
110
|
+
|
|
111
|
+
Returns ``[]`` when no id can be derived (no detectors configured, or a
|
|
112
|
+
config the factory rejects) — the caller then falls back to unfiltered.
|
|
113
|
+
"""
|
|
114
|
+
ids: list[str] = []
|
|
115
|
+
for detector_config in config.detectors:
|
|
116
|
+
try:
|
|
117
|
+
params = detector_config.get_algorithm_params()
|
|
118
|
+
seasonality = detector_config.get_seasonality_components()
|
|
119
|
+
if seasonality is not None:
|
|
120
|
+
params["seasonality_components"] = seasonality
|
|
121
|
+
detector = DetectorFactory.create_from_config(
|
|
122
|
+
{"type": detector_config.type, "params": params}
|
|
123
|
+
)
|
|
124
|
+
ids.append(detector.get_detector_id())
|
|
125
|
+
except Exception: # noqa: BLE001 — fall back to unfiltered on any bad config
|
|
126
|
+
return []
|
|
127
|
+
return ids
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_window_detections(
|
|
131
|
+
internal: InternalTablesManager,
|
|
132
|
+
metric_name: str,
|
|
133
|
+
detector_ids: list[str],
|
|
134
|
+
start: datetime,
|
|
135
|
+
to_exclusive: datetime,
|
|
136
|
+
) -> list[dict[str, Any]]:
|
|
137
|
+
"""Window detections for the given detector ids (all ids when empty).
|
|
138
|
+
|
|
139
|
+
Per-id queries push the filter into SQL instead of transferring every
|
|
140
|
+
historical generation's rows; the merged result is re-sorted to the
|
|
141
|
+
global ``(timestamp, detector_id)`` order ``load_detections`` guarantees,
|
|
142
|
+
which the replay walk expects.
|
|
143
|
+
"""
|
|
144
|
+
if not detector_ids:
|
|
145
|
+
return internal.load_detections(metric_name, None, start, to_exclusive)
|
|
146
|
+
rows: list[dict[str, Any]] = []
|
|
147
|
+
for detector_id in detector_ids:
|
|
148
|
+
rows.extend(internal.load_detections(metric_name, detector_id, start, to_exclusive))
|
|
149
|
+
rows.sort(key=lambda r: (r["timestamp"], r["detector_id"]))
|
|
150
|
+
return rows
|
|
151
|
+
|
|
152
|
+
|
|
98
153
|
def _resolve_metric_window(
|
|
99
154
|
internal: InternalTablesManager,
|
|
100
155
|
metric_name: str,
|
|
@@ -329,7 +384,9 @@ def _fill_stats(
|
|
|
329
384
|
row["points"] = n_points
|
|
330
385
|
row["first_point_in_window"] = _ms(ts_arr[0]) if n_points else None
|
|
331
386
|
|
|
332
|
-
det_rows =
|
|
387
|
+
det_rows = _load_window_detections(
|
|
388
|
+
internal, name, _configured_detector_ids(config), start, to_exclusive
|
|
389
|
+
)
|
|
333
390
|
det_rows = [r for r in det_rows if _ms(r["timestamp"]) <= end_ms]
|
|
334
391
|
|
|
335
392
|
anomalous_timestamps: set[int] = set()
|
|
@@ -386,6 +443,48 @@ def _fill_stats(
|
|
|
386
443
|
row["spark_anoms"] = _downsample_evenly(sorted(anomalous_timestamps), _MAX_SPARK_ANOMS)
|
|
387
444
|
|
|
388
445
|
|
|
446
|
+
def resolve_project_budget(project_config: ProjectConfig) -> float:
|
|
447
|
+
"""The project-level ``false_alert_budget`` fallback (metric overrides it)."""
|
|
448
|
+
if project_config.false_alert_budget is not None:
|
|
449
|
+
return project_config.false_alert_budget
|
|
450
|
+
return DEFAULT_FALSE_ALERT_BUDGET
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def build_metric_row(
|
|
454
|
+
*,
|
|
455
|
+
project_config: ProjectConfig,
|
|
456
|
+
project_root: Path,
|
|
457
|
+
metric_path: Path,
|
|
458
|
+
config: MetricConfig,
|
|
459
|
+
internal: InternalTablesManager,
|
|
460
|
+
window_preset: str,
|
|
461
|
+
now: datetime | None = None,
|
|
462
|
+
) -> dict[str, Any]:
|
|
463
|
+
"""One metric's overview row — the unit ``GET /api/stats/<name>`` serves.
|
|
464
|
+
|
|
465
|
+
The cockpit fetches rows one metric at a time (bounded, seconds each)
|
|
466
|
+
instead of one monolithic all-metrics payload: on a production-sized
|
|
467
|
+
project the combined read (full detections of every metric × the window,
|
|
468
|
+
serialized on the single DB connection) takes minutes — long enough for
|
|
469
|
+
the browser to abort the request as failed while the page shows an
|
|
470
|
+
endless spinner. Raises ``ValueError`` for an unknown *window_preset*.
|
|
471
|
+
"""
|
|
472
|
+
if window_preset not in ALL_WINDOW_PRESETS:
|
|
473
|
+
allowed = ", ".join(sorted(ALL_WINDOW_PRESETS))
|
|
474
|
+
raise ValueError(f"Unknown window preset {window_preset!r}. Choose one of: {allowed}.")
|
|
475
|
+
return _build_metric_row(
|
|
476
|
+
project_root=project_root,
|
|
477
|
+
metrics_dir=project_root / "metrics",
|
|
478
|
+
metric_path=metric_path,
|
|
479
|
+
config=config,
|
|
480
|
+
internal=internal,
|
|
481
|
+
window_preset=window_preset,
|
|
482
|
+
now=now if now is not None else now_utc_naive(),
|
|
483
|
+
project_name=project_config.name,
|
|
484
|
+
project_budget=resolve_project_budget(project_config),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
389
488
|
def _build_metric_row(
|
|
390
489
|
*,
|
|
391
490
|
project_root: Path,
|
|
@@ -444,11 +543,7 @@ def build_overview_payload(
|
|
|
444
543
|
|
|
445
544
|
now_dt = now if now is not None else now_utc_naive()
|
|
446
545
|
metrics_dir = project_root / "metrics"
|
|
447
|
-
project_budget = (
|
|
448
|
-
project_config.false_alert_budget
|
|
449
|
-
if project_config.false_alert_budget is not None
|
|
450
|
-
else DEFAULT_FALSE_ALERT_BUDGET
|
|
451
|
-
)
|
|
546
|
+
project_budget = resolve_project_budget(project_config)
|
|
452
547
|
|
|
453
548
|
rows = [
|
|
454
549
|
_build_metric_row(
|