detectkit 0.49.1__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.1/detectkit.egg-info → detectkit-0.49.2}/PKG-INFO +1 -1
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/__init__.py +1 -1
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_decision.py +2 -1
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_replay.py +24 -7
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/assets/ui.js +10 -4
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/server.py +6 -1
- {detectkit-0.49.1 → detectkit-0.49.2/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.49.1 → detectkit-0.49.2}/LICENSE +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/MANIFEST.in +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/README.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/result.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/_output.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/main.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/profile.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/project_config.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/config/validator.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/interval.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/core/models.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/database/tables.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/html.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/tuning/server.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/html.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/ui/overview.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit/utils/stats.py +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/pyproject.toml +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/requirements.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.49.2}/setup.cfg +0 -0
- {detectkit-0.49.1 → 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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
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
2
|
.${Y}{
|
|
3
3
|
--clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;
|
|
4
4
|
--muted:#6e675b;--faint:#9a9384;
|
|
@@ -160,8 +160,14 @@
|
|
|
160
160
|
.dtk-ui-overlay-close{border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
161
161
|
border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:14px;}
|
|
162
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;}
|
|
163
|
+
.dtk-ui-overlay-body{flex:1;min-height:0;position:relative;background:var(--term-bg);}
|
|
164
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);}}
|
|
165
171
|
|
|
166
172
|
/* --- drawers (run panel + jobs) --------------------------------------------- */
|
|
167
173
|
.dtk-ui-drawer-backdrop{position:fixed;inset:0;background:rgba(10,9,7,0.5);z-index:40;display:none;}
|
|
@@ -244,6 +250,6 @@
|
|
|
244
250
|
.dtk-ui-row2{grid-template-columns:1fr;}
|
|
245
251
|
.dtk-ui-table-wrap{overflow-x:auto;}
|
|
246
252
|
}
|
|
247
|
-
`,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,
|
|
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"}
|
|
248
254
|
alert rule: ${n}
|
|
249
|
-
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 v=document.createElement("td");v.className="dtk-ui-namecell";let w=e.error?`<span class="dtk-ui-err-badge" title="${k(e.error)}">!</span>`:"";v.title=Ze(e),v.innerHTML=`<span class="dtk-ui-name">${k(e.name)}</span>${w}${Qe(e.tags)}`,o.appendChild(v);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 h=document.createElement("td");if(h.className="dtk-ui-alertscell",e.pending)h.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>`:"";h.innerHTML=`<span class="${S}">${e.alerts.anomaly}</span>${E}`}o.appendChild(h);let g=document.createElement("td");e.pending?g.innerHTML=ue:e.alerts.last_ts!==null?g.innerHTML=`<span class="dtk-ui-lastalert" title="${k(me(e.alerts.last_ts))} UTC">${k(le(n,e.alerts.last_ts))}</span>`:g.innerHTML='<span class="dtk-ui-lastalert">\u2014</span>',o.appendChild(g);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 b=document.createElement("td");b.innerHTML=e.pending?ue:Ve(e),o.appendChild(b);let x=document.createElement("td");x.innerHTML=e.locked?'<span class="dtk-ui-lock" title="pipeline lock currently held for this metric">LOCK</span>':"",o.appendChild(x);let M=document.createElement("td");M.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 $=document.createElement("button");$.type="button",$.className="dtk-ui-actionbtn",$.textContent="Tune",$.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),M.append(u,$,l),o.appendChild(M),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 v=Ae(a,n.key),w=Ae(c,n.key);return v<w?-1*o:v>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),v=[...c.keys()].sort((p,m)=>p===m?0:p===""?-1:m===""?1:p.localeCompare(m));for(let p of v){let m=(w=c.get(p))!=null?w:[],h=document.createElement("div");h.className="dtk-ui-group";let g=m.reduce((u,$)=>u+$.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 ${g} alert${g===1?"":"s"}</span>`,h.appendChild(s);let b=document.createElement("table");b.className="dtk-ui-table";let x=document.createElement("thead");x.appendChild(ot(n,t)),b.appendChild(x);let M=document.createElement("tbody");for(let u of tt(m,n))M.appendChild(et(u,r,t,o));b.appendChild(M),h.appendChild(b),a.appendChild(h)}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 v=document.createElement("div");v.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)",v.append(w,p),c.appendChild(v),a.appendChild(c);let m=document.createElement("div");m.className="dtk-ui-overlay-body";let h=document.createElement("iframe");h.src=be(n,r),h.title=`detectkit report \u2014 ${n}`,m.appendChild(h),a.appendChild(m),e.appendChild(o);function g(){document.removeEventListener("keydown",s),o.remove(),t.onClose()}function s(b){b.key==="Escape"&&g()}return o.addEventListener("click",b=>{b.target===o&&g()}),p.onclick=g,document.addEventListener("keydown",s),{setWindow(b){let x=c.querySelector(".dtk-ui-overlay-sub");x&&(x.textContent=`window: ${b}`),h.src=be(n,b)},close:g}}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 v=document.createElement("div");v.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),v.innerHTML='<span class="dtk-ui-field-label">Select</span>',v.append(p,w),c.appendChild(v);let m=document.createElement("div");m.className="dtk-ui-field",m.innerHTML='<span class="dtk-ui-field-label">Steps</span>';let h=document.createElement("div");h.className="dtk-ui-checks";let g={};for(let T of["load","detect","alert"]){let L=document.createElement("label");L.className="dtk-ui-check";let N=document.createElement("input");N.type="checkbox",N.checked=!0,N.onchange=H,L.append(N,document.createTextNode(T)),h.appendChild(L),g[T]=N}m.appendChild(h),c.appendChild(m);let s=document.createElement("div");s.className="dtk-ui-row2";let b=document.createElement("div");b.className="dtk-ui-field",b.innerHTML='<span class="dtk-ui-field-label">From</span>';let x=document.createElement("input");x.type="text",x.className="dtk-ui-input",x.placeholder="YYYY-MM-DD [HH:MM:SS]",x.oninput=H,b.appendChild(x);let M=document.createElement("div");M.className="dtk-ui-field",M.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,M.appendChild(u),s.append(b,M),c.appendChild(s);let $=document.createElement("div");$.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")),$.append(l,S),c.appendChild($);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(g).filter(T=>g[T].checked)}function H(){let T=n.isPipelineBusy(),L=p.value.trim(),N=x.value.trim()===""||ze.test(x.value.trim()),i=u.value.trim()===""||ze.test(u.value.trim()),d="";T.busy?d=T.reason:L===""?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=T.busy||L===""||!N||!i,_.disabled=T.busy||L===""}function ie(){return{select:p.value.trim(),steps:te(),from:x.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:x.value.trim()||null,to:u.value.trim()||null}),_.onclick=()=>{let T=p.value.trim();window.confirm(`Unlock the pipeline lock for "${T}"? Only do this if you're sure no dtk process is actually running against it.`)&&n.submitUnlock({select:T})};function ne(){r.classList.remove("open"),t.classList.remove("open")}return a.onclick=ne,r.onclick=ne,e.append(r,t),{el:t,open(T){T&&(p.value=T),r.classList.add("open"),t.classList.add("open"),H()},close:ne,isOpen(){return t.classList.contains("open")},refreshOptions(){w.innerHTML=["*",...n.getSelectOptions()].map(T=>`<option value="${k(T)}"></option>`).join("")},refreshBusyState:H,resetLog(){q=[],ee(),z.textContent="",z.className="dtk-ui-log-line"},appendLog(T){if(T.length===0)return;let L=G();q.push(...T),ee(),L&&(A.scrollTop=A.scrollHeight)},setLogStatus(T,L){if(T==="running")return;let N=T==="done"&&L===0?"exit-ok":T==="stopped"?"exit-stop":"exit-fail";z.className=`dtk-ui-log-line ${N}`,z.textContent=`\u2500\u2500 ${T} (exit ${L!=null?L:"?"}) \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 v=document.createElement("div");v.className="dtk-ui-joblist",c.appendChild(v);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,h){var g;if(p.length===0){v.innerHTML='<div class="dtk-ui-empty">No jobs yet.</div>';return}v.innerHTML="";for(let s of p){let b=document.createElement("div");b.className="dtk-ui-jobrow"+(s.id===h?" active":"");let x=s.status==="running"?" pulse":"",M=he(s.started_at,(g=s.finished_at)!=null?g: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${x}" 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(M)}</span>`,b.appendChild(u);let $=document.createElement("div");$.className="dtk-ui-jobrow-label",$.textContent=s.label,b.appendChild($);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&&b.appendChild(l),b.onclick=()=>n.onFollow(s.id),v.appendChild(b)}}}}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 v(){let i=new Set,d=new Set;for(let C of e.metrics){i.add(C.name);for(let j of C.tags)d.add(j)}return[...i,...[...d].map(C=>`tag:${C}`)]}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 h=document.createElement("div");h.className="dtk-ui-header-right",p.appendChild(h);let g=document.createElement("div");g.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,g.querySelectorAll(".dtk-ui-seg-btn").forEach(C=>C.classList.remove("on")),d.classList.add("on"),o&&o.setWindow(i.value),N())},g.appendChild(d)}h.appendChild(g);let s=document.createElement("button");s.type="button",s.className="dtk-ui-iconbtn",s.title="Refresh overview",s.textContent="\u27F3",s.onclick=()=>{N()},h.appendChild(s);let b=document.createElement("button");b.type="button",b.className="dtk-ui-runbtn",b.textContent="Run pipeline",b.onclick=()=>P(),h.appendChild(b);let x=document.createElement("span");x.className="dtk-ui-progresschip",x.style.display="none",h.appendChild(x);function M(i,d){if(d===0||i>=d){x.style.display="none";return}x.textContent=`${i}/${d}`,x.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()},h.appendChild(u);function $(){let i=t.jobs.find(j=>j.status==="running");u.classList.toggle("running",!!i);let d=i?`${i.kind} ${i.label}`:"idle",C=u.querySelector("span:last-child");C&&(C.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),C=d.filter(y=>y.error!==null);if(d.length===i&&C.length===i){let y=document.createElement("div");y.className="dtk-ui-banner";let se=(U=(Q=C[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:v,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,C,j){let X={id:C,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!==C)],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 $e({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 Le(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(),T(i)}function te(){$(),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(C=>C.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 T(i){ne();let d=()=>{Ce(i,t.followOffset).then(C=>{if(t.followedJobId===i){if(E.appendLog(C.lines),t.followOffset=C.next_offset,C.status!=="running"){E.setLogStatus(C.status,C.returncode),c=void 0,H();return}c=window.setTimeout(d,1e3)}}).catch(C=>{B(r,"error",`job ${i}: ${C.message}`),c=void 0})};c=window.setTimeout(d,0)}let L=0;async function N(){let i=++L,d=t.windowPreset;if(t.metrics=e.metrics.map(pe),s.classList.add("spinning"),M(0,e.metrics.length),f(),e.metrics.length===0){s.classList.remove("spinning"),M(0,0);return}let C=[...e.metrics],j=0;async function X(){for(;;){if(i!==L)return;let U=C.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!==L)return;let se=t.metrics.findIndex(J=>J.name===U.name);se!==-1&&(t.metrics[se]=y),j++,M(j,e.metrics.length),f()}}let Q=Math.min(it,e.metrics.length);await Promise.all(Array.from({length:Q},()=>X())),i===L&&(s.classList.remove("spinning"),M(j,e.metrics.length),E.refreshOptions())}f(),E.refreshOptions(),N(),H()}window.__DTK_UI__={render:lt};})();
|
|
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};})();
|
|
@@ -519,8 +519,13 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
519
519
|
|
|
520
520
|
Mirrors ``tuning/server.py``: the status line is latin-1 only, and an
|
|
521
521
|
exception message can carry non-ASCII (e.g. an ``≈`` from a validation
|
|
522
|
-
error), so the detail rides in the body instead.
|
|
522
|
+
error), so the detail rides in the body instead. Errors other than the
|
|
523
|
+
routine bad-token 403 are also echoed to the terminal — a page stuck on
|
|
524
|
+
a failing request should be diagnosable without opening devtools.
|
|
523
525
|
"""
|
|
526
|
+
if code >= 400 and code != 403:
|
|
527
|
+
first_line = detail.splitlines()[0] if detail else ""
|
|
528
|
+
self._srv().echo(f" [ui] {code} {urlparse(self.path).path}: {first_line}")
|
|
524
529
|
body = detail.encode("utf-8")
|
|
525
530
|
self.send_response(code)
|
|
526
531
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.49.1 → detectkit-0.49.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|