detectkit 0.21.0__tar.gz → 0.22.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {detectkit-0.21.0/detectkit.egg-info → detectkit-0.22.0}/PKG-INFO +1 -1
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/__init__.py +1 -1
- detectkit-0.22.0/detectkit/autotune/html_labeler.py +362 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/autotune.md +17 -11
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +10 -5
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/autotune.py +31 -1
- {detectkit-0.21.0 → detectkit-0.22.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.21.0/detectkit/autotune/html_labeler.py +0 -219
- {detectkit-0.21.0 → detectkit-0.22.0}/LICENSE +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/MANIFEST.in +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/README.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/core/models.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/pyproject.toml +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/requirements.txt +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/setup.cfg +0 -0
- {detectkit-0.21.0 → detectkit-0.22.0}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.22.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Generate a self-contained HTML labeler for a metric series.
|
|
2
|
+
|
|
3
|
+
Emits a single HTML file (inline CSS/JS, no CDN) with the series embedded as a
|
|
4
|
+
JSON literal: a zoomable/pannable canvas chart where the user click-drags over
|
|
5
|
+
incident spans (with an optional per-incident description) and exports a labels
|
|
6
|
+
file in the canonical schema, fed back via
|
|
7
|
+
``dtk autotune --select <metric> --incidents <file-or-dir>``.
|
|
8
|
+
|
|
9
|
+
The page is offline-only — a browser cannot write to the project, so Export
|
|
10
|
+
downloads a **versioned** file (``<metric>-<UTC-stamp>.yml``); drop it into
|
|
11
|
+
``incidents/<metric>/`` to keep every labeling round (``--incidents`` accepts
|
|
12
|
+
that directory and uses the newest version).
|
|
13
|
+
|
|
14
|
+
Docs sync: the autotune reference page + landing embed a *live* copy of this
|
|
15
|
+
output (``docs/examples/autotune-labeler.html``) so the site always shows the
|
|
16
|
+
real UI. After changing the template below, regenerate that example so the docs
|
|
17
|
+
don't drift: ``python website/scripts/gen-labeler-example.py`` (also in the
|
|
18
|
+
release checklist).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from detectkit.utils.json_utils import json_dumps_sorted
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ts_to_str(ts64: np.datetime64) -> str:
|
|
31
|
+
ms = int(ts64.astype("datetime64[ms]").astype(np.int64))
|
|
32
|
+
return (datetime(1970, 1, 1) + timedelta(milliseconds=ms)).strftime("%Y-%m-%d %H:%M:%S")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Built with .replace() (not .format()), so braces are literal — keep them single.
|
|
36
|
+
# Self-contained: inline brand styling/logo/JS, no network. Palette + fonts mirror
|
|
37
|
+
# website/src/styles/brand.css (.claude/rules/design.md); incident bands use the
|
|
38
|
+
# anomaly status color, the drag preview the no-data color.
|
|
39
|
+
_TEMPLATE = """<!doctype html>
|
|
40
|
+
<meta charset="utf-8">
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
42
|
+
<title>detectkit · label incidents · __METRIC__</title>
|
|
43
|
+
<style>
|
|
44
|
+
:root {
|
|
45
|
+
--clay:#d15b36; --clay-700:#b4471f; --paper:#f5f1e8; --muted:#6e675b; --faint:#9a9384;
|
|
46
|
+
--term-bg:#211e1a; --term-surface:#1b1916; --term-border:#332f29; --term-text:#c9c2b4;
|
|
47
|
+
--anomaly:#d63232; --nodata:#f0ad4e;
|
|
48
|
+
--ui:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
49
|
+
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
|
|
50
|
+
}
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
body { font-family: var(--ui); margin: 0; background: var(--term-bg); color: var(--term-text);
|
|
53
|
+
-webkit-font-smoothing: antialiased; }
|
|
54
|
+
.shell { max-width: 1080px; margin: 0 auto; padding: 22px 22px 44px; }
|
|
55
|
+
.brand { display:flex; align-items:center; gap:9px; margin-bottom: 14px; }
|
|
56
|
+
.brand svg { width: 26px; height: 26px; border-radius: 7px; display:block; }
|
|
57
|
+
.brand b { color: var(--paper); font-weight: 600; font-size: 15px; letter-spacing: .2px; }
|
|
58
|
+
.brand span { color: var(--faint); font-size: 12px; }
|
|
59
|
+
h1 { font-size: 18px; line-height: 1.3; margin: 0 0 6px; color: var(--paper); font-weight: 600; }
|
|
60
|
+
h1 code { color: var(--clay); font-family: var(--mono); font-size: .82em; }
|
|
61
|
+
.hint { color: var(--faint); font-size: 13px; margin: 0 0 18px; line-height: 1.55; }
|
|
62
|
+
.hint code, code.k { color: var(--term-text); font-family: var(--mono); font-size: 12px;
|
|
63
|
+
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 5px; padding: 1px 6px; }
|
|
64
|
+
.toolbar { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom: 12px; }
|
|
65
|
+
button { font-family: var(--ui); font-size: 13px; font-weight: 500; border: 0; border-radius: 7px;
|
|
66
|
+
padding: 9px 15px; cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
|
|
67
|
+
button.primary { background: var(--clay); color: #fff; }
|
|
68
|
+
button.primary:hover { background: var(--clay-700); }
|
|
69
|
+
button.ghost { background: transparent; color: var(--term-text); border: 1px solid var(--term-border); }
|
|
70
|
+
button.ghost:hover { border-color: var(--faint); color: var(--paper); }
|
|
71
|
+
.summary { margin-left: auto; color: var(--faint); font-size: 12.5px; font-family: var(--mono); }
|
|
72
|
+
.summary b { color: var(--clay); font-weight: 600; }
|
|
73
|
+
canvas#c { width: 100%; height: clamp(300px, 44vh, 500px); display:block; touch-action: none;
|
|
74
|
+
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
|
|
75
|
+
.zoombar { display:flex; align-items:center; gap:8px; margin: 10px 0 6px; }
|
|
76
|
+
.rangelbl { margin-left: auto; color: var(--faint); font-size: 12px; font-family: var(--mono); }
|
|
77
|
+
canvas#ov { width: 100%; height: 66px; display:block; touch-action: none;
|
|
78
|
+
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: grab; }
|
|
79
|
+
.navhint { color: var(--faint); font-size: 12px; margin: 7px 2px 0; }
|
|
80
|
+
.empty { color: var(--faint); font-size: 13px; margin: 18px 2px; font-style: italic; }
|
|
81
|
+
ul { list-style: none; margin: 16px 0 0; padding: 0; }
|
|
82
|
+
li { display:flex; align-items:center; gap:11px; padding: 9px 12px; font-size: 13px; flex-wrap: wrap;
|
|
83
|
+
border: 1px solid var(--term-border); border-radius: 8px; margin-bottom: 7px; background: var(--term-surface); }
|
|
84
|
+
li .dot { width:9px; height:9px; border-radius:50%; background: var(--anomaly); flex: 0 0 auto; }
|
|
85
|
+
li .span { font-family: var(--mono); color: var(--term-text); }
|
|
86
|
+
li .dur { color: var(--faint); font-size: 12px; }
|
|
87
|
+
li input.desc { flex: 1 1 220px; min-width: 160px; background: var(--term-bg); color: var(--paper);
|
|
88
|
+
border: 1px solid var(--term-border); border-radius: 6px; padding: 6px 9px; font-family: var(--ui); font-size: 12.5px; }
|
|
89
|
+
li input.desc::placeholder { color: var(--muted); }
|
|
90
|
+
li input.desc:focus { outline: none; border-color: var(--clay); }
|
|
91
|
+
li button { margin-left: auto; padding: 5px 11px; font-size: 12px; }
|
|
92
|
+
footer { margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--term-border);
|
|
93
|
+
color: var(--faint); font-size: 12px; line-height: 1.6; }
|
|
94
|
+
footer code { font-family: var(--mono); color: var(--term-text); }
|
|
95
|
+
</style>
|
|
96
|
+
<div class="shell">
|
|
97
|
+
<div class="brand">
|
|
98
|
+
<svg viewBox="0 0 100 100" aria-hidden="true"><rect x="3" y="3" width="94" height="94" rx="26" fill="#D15B36"/><polyline points="14,62 36,62 50,22 64,62 86,62" fill="none" stroke="#FBF9F3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="50" cy="22" r="6.5" fill="#FBF9F3"/></svg>
|
|
99
|
+
<b>detectkit</b><span>· incident labeler</span>
|
|
100
|
+
</div>
|
|
101
|
+
<h1>Label incidents — <code>__METRIC__</code></h1>
|
|
102
|
+
<p class="hint">Click-drag across the chart to mark each real incident, add a short description, then
|
|
103
|
+
<b>Export</b>. Save the file into <code class="k">incidents/__METRIC__/</code> and run
|
|
104
|
+
<code class="k">dtk autotune --select __METRIC__ --incidents incidents/__METRIC__/</code></p>
|
|
105
|
+
<div class="toolbar">
|
|
106
|
+
<button id="export" class="primary">Export labels</button>
|
|
107
|
+
<button id="clear" class="ghost">Clear all</button>
|
|
108
|
+
<span id="summary" class="summary"></span>
|
|
109
|
+
</div>
|
|
110
|
+
<canvas id="c" aria-label="metric series — drag to mark an incident, scroll to zoom"></canvas>
|
|
111
|
+
<div class="zoombar">
|
|
112
|
+
<button id="zreset" class="ghost">Reset zoom</button>
|
|
113
|
+
<span id="range" class="rangelbl"></span>
|
|
114
|
+
</div>
|
|
115
|
+
<canvas id="ov" aria-label="navigator — drag the window to pan, its edges to stretch the view"></canvas>
|
|
116
|
+
<div class="navhint">Scroll to zoom where you point · double-click to reset · drag the navigator window
|
|
117
|
+
below to move, or drag its edges to stretch / squeeze the view.</div>
|
|
118
|
+
<div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
|
|
119
|
+
<ul id="list"></ul>
|
|
120
|
+
<footer>All times UTC · self-contained, nothing leaves your browser · re-label any time —
|
|
121
|
+
exports are versioned (<code>__METRIC__-<timestamp>.yml</code>), so keep every round in
|
|
122
|
+
<code>incidents/__METRIC__/</code>. Generated by <code>dtk autotune --label</code>.</footer>
|
|
123
|
+
</div>
|
|
124
|
+
<script>
|
|
125
|
+
const DATA = __PAYLOAD__;
|
|
126
|
+
const pts = DATA.points.map(p => ({ts: Date.parse(p.t.replace(' ','T')+'Z'), v: p.v}));
|
|
127
|
+
const N = pts.length;
|
|
128
|
+
const vraw = pts.filter(p => p.v !== null).map(p => p.v);
|
|
129
|
+
const vmin0 = vraw.length ? Math.min.apply(null, vraw) : 0;
|
|
130
|
+
const vmax0 = vraw.length ? Math.max.apply(null, vraw) : 1;
|
|
131
|
+
const vpad = (vmax0 - vmin0) * 0.06 || 1;
|
|
132
|
+
const vmin = vmin0 - vpad, vmax = vmax0 + vpad;
|
|
133
|
+
const tmin = N ? pts[0].ts : 0, tmax = N ? pts[N-1].ts : 1, fullSpan = (tmax - tmin) || 1;
|
|
134
|
+
const step = fullSpan / Math.max(1, N - 1);
|
|
135
|
+
const minSpan = Math.max(step * 8, 1000);
|
|
136
|
+
let viewMin = tmin, viewMax = tmax;
|
|
137
|
+
const incidents = [];
|
|
138
|
+
const c = document.getElementById('c'), ov = document.getElementById('ov');
|
|
139
|
+
const ctx = c.getContext('2d'), octx = ov.getContext('2d');
|
|
140
|
+
const M = {l:56, r:16, t:14, b:30}, OM = {l:56, r:16, t:8, b:8};
|
|
141
|
+
let dpr = 1, hover = null, dragging = null, ovAct = null;
|
|
142
|
+
|
|
143
|
+
const clamp = (x,a,b) => Math.max(a, Math.min(b, x));
|
|
144
|
+
const vspan = () => viewMax - viewMin;
|
|
145
|
+
const plotW = () => c.width - (M.l+M.r)*dpr;
|
|
146
|
+
const plotH = () => c.height - (M.t+M.b)*dpr;
|
|
147
|
+
const px = ts => M.l*dpr + (ts-viewMin)/(vspan()||1)*plotW();
|
|
148
|
+
const py = v => (c.height - M.b*dpr) - (v-vmin)/((vmax-vmin)||1)*plotH();
|
|
149
|
+
const ovWd = () => ov.width - (OM.l+OM.r)*dpr;
|
|
150
|
+
const ovHt = () => ov.height - (OM.t+OM.b)*dpr;
|
|
151
|
+
const ovpx = ts => OM.l*dpr + (ts-tmin)/fullSpan*ovWd();
|
|
152
|
+
const ovpy = v => (ov.height - OM.b*dpr) - (v-vmin)/((vmax-vmin)||1)*ovHt();
|
|
153
|
+
const pad2 = n => String(n).padStart(2,'0');
|
|
154
|
+
const fmtTs = ts => new Date(ts).toISOString().slice(0,19).replace('T',' ');
|
|
155
|
+
const fmtTick = (ts, sp) => { const s = new Date(ts).toISOString();
|
|
156
|
+
return sp < 2*86400000 ? s.slice(5,16).replace('T',' ') : s.slice(5,10); };
|
|
157
|
+
const fmtVal = v => { const a = Math.abs(v); return a>=1000 ? v.toFixed(0) : a>=10 ? v.toFixed(1) : v.toFixed(2); };
|
|
158
|
+
function fmtDur(ms) { const m = Math.round(ms/60000); if (m<60) return m+'m';
|
|
159
|
+
const h = Math.floor(m/60), mm = m%60; if (h<24) return h+'h'+(mm?(' '+mm+'m'):'');
|
|
160
|
+
const d = Math.floor(h/24), hh = h%24; return d+'d'+(hh?(' '+hh+'h'):''); }
|
|
161
|
+
const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
162
|
+
const yamlStr = s => '"' + String(s).replace(/\\\\/g,'\\\\\\\\').replace(/"/g,'\\\\"') + '"';
|
|
163
|
+
|
|
164
|
+
function setView(a, b) {
|
|
165
|
+
let s = b - a;
|
|
166
|
+
if (s < minSpan) { const m=(a+b)/2; a=m-minSpan/2; b=m+minSpan/2; s=minSpan; }
|
|
167
|
+
if (s >= fullSpan) { a=tmin; b=tmax; }
|
|
168
|
+
if (a < tmin) { b += tmin-a; a=tmin; }
|
|
169
|
+
if (b > tmax) { a -= b-tmax; b=tmax; }
|
|
170
|
+
viewMin = clamp(a, tmin, tmax); viewMax = clamp(b, tmin, tmax);
|
|
171
|
+
drawAll();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Series drawing with min/max decimation (one envelope segment per pixel column)
|
|
175
|
+
// so a 100k-point series stays fast and spikes stay visible; direct polyline when
|
|
176
|
+
// few points are visible (zoomed in).
|
|
177
|
+
function drawSeries(ctx2, xfn, yfn, lo, hi, leftDev, widthDev, color, lw) {
|
|
178
|
+
const cols = Math.max(1, Math.round(widthDev)), sp = (hi-lo)||1;
|
|
179
|
+
let vis = 0;
|
|
180
|
+
for (let i=0;i<N;i++) { const p=pts[i]; if (p.v===null||p.ts<lo||p.ts>hi) continue; vis++; }
|
|
181
|
+
ctx2.strokeStyle = color; ctx2.lineWidth = lw*dpr; ctx2.beginPath();
|
|
182
|
+
if (vis <= cols) {
|
|
183
|
+
let pen = false;
|
|
184
|
+
for (let i=0;i<N;i++) { const p=pts[i];
|
|
185
|
+
if (p.v===null || p.ts<lo || p.ts>hi) { pen=false; continue; }
|
|
186
|
+
const X=xfn(p.ts), Y=yfn(p.v);
|
|
187
|
+
if (!pen) { ctx2.moveTo(X,Y); pen=true; } else ctx2.lineTo(X,Y);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
const cmin = new Array(cols).fill(null), cmax = new Array(cols).fill(null);
|
|
191
|
+
for (let i=0;i<N;i++) { const p=pts[i]; if (p.v===null||p.ts<lo||p.ts>hi) continue;
|
|
192
|
+
let col = Math.floor((p.ts-lo)/sp*(cols-1)); col = col<0?0:col>cols-1?cols-1:col;
|
|
193
|
+
if (cmin[col]===null||p.v<cmin[col]) cmin[col]=p.v;
|
|
194
|
+
if (cmax[col]===null||p.v>cmax[col]) cmax[col]=p.v;
|
|
195
|
+
}
|
|
196
|
+
let pen = false;
|
|
197
|
+
for (let col=0;col<cols;col++) { if (cmax[col]===null) { pen=false; continue; }
|
|
198
|
+
const X=leftDev+col, yh=yfn(cmax[col]), yl=yfn(cmin[col]);
|
|
199
|
+
if (!pen) { ctx2.moveTo(X,yh); pen=true; } else ctx2.lineTo(X,yh);
|
|
200
|
+
ctx2.lineTo(X,yl);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
ctx2.stroke();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function draw() {
|
|
207
|
+
ctx.clearRect(0,0,c.width,c.height);
|
|
208
|
+
ctx.font = (11*dpr)+'px ui-sans-serif, system-ui, sans-serif';
|
|
209
|
+
ctx.textBaseline = 'middle';
|
|
210
|
+
for (let i=0;i<=4;i++) { const v=vmin+(vmax-vmin)*i/4, yy=py(v);
|
|
211
|
+
ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1*dpr;
|
|
212
|
+
ctx.beginPath(); ctx.moveTo(M.l*dpr,yy); ctx.lineTo(c.width-M.r*dpr,yy); ctx.stroke();
|
|
213
|
+
ctx.fillStyle='#6e675b'; ctx.textAlign='right'; ctx.fillText(fmtVal(v),(M.l-8)*dpr,yy); }
|
|
214
|
+
ctx.textBaseline = 'top';
|
|
215
|
+
for (let i=0;i<=5;i++) { const ts=viewMin+vspan()*i/5, xx=px(ts);
|
|
216
|
+
ctx.fillStyle='#6e675b'; ctx.textAlign=i===0?'left':i===5?'right':'center';
|
|
217
|
+
ctx.fillText(fmtTick(ts,vspan()), xx, (c.height-M.b+8)*dpr); }
|
|
218
|
+
ctx.save(); ctx.beginPath(); ctx.rect(M.l*dpr, M.t*dpr, plotW(), plotH()); ctx.clip();
|
|
219
|
+
incidents.forEach(iv => { const x0=px(iv.a), x1=px(iv.b);
|
|
220
|
+
ctx.fillStyle='rgba(214,50,50,0.20)'; ctx.fillRect(x0, M.t*dpr, x1-x0, plotH());
|
|
221
|
+
ctx.strokeStyle='rgba(214,50,50,0.55)'; ctx.lineWidth=1*dpr; ctx.strokeRect(x0, M.t*dpr, x1-x0, plotH()); });
|
|
222
|
+
if (dragging) { const x0=px(dragging.a), x1=px(dragging.b);
|
|
223
|
+
ctx.fillStyle='rgba(240,173,78,0.28)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); }
|
|
224
|
+
drawSeries(ctx, px, py, viewMin, viewMax, M.l*dpr, plotW(), '#d15b36', 1.5);
|
|
225
|
+
ctx.restore();
|
|
226
|
+
if (hover && !dragging && !ovAct) drawHover();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function drawHover() {
|
|
230
|
+
let best=null, bd=Infinity;
|
|
231
|
+
for (let i=0;i<N;i++) { const p=pts[i]; if (p.v===null||p.ts<viewMin||p.ts>viewMax) continue;
|
|
232
|
+
const d=Math.abs(p.ts-hover.ts); if (d<bd) { bd=d; best=p; } }
|
|
233
|
+
if (!best) return;
|
|
234
|
+
const X=px(best.ts), Y=py(best.v);
|
|
235
|
+
ctx.strokeStyle='rgba(201,194,180,0.25)'; ctx.lineWidth=1*dpr;
|
|
236
|
+
ctx.beginPath(); ctx.moveTo(X, M.t*dpr); ctx.lineTo(X, c.height-M.b*dpr); ctx.stroke();
|
|
237
|
+
ctx.fillStyle='#f5f1e8'; ctx.beginPath(); ctx.arc(X,Y,3*dpr,0,7); ctx.fill();
|
|
238
|
+
const label=fmtTick(best.ts, 0)+' · '+fmtVal(best.v);
|
|
239
|
+
ctx.font=(11*dpr)+'px ui-monospace, monospace'; const tw=ctx.measureText(label).width;
|
|
240
|
+
let bx=X+8*dpr; if (bx+tw+12*dpr > c.width) bx=X-tw-20*dpr;
|
|
241
|
+
ctx.fillStyle='rgba(27,25,22,0.92)'; ctx.strokeStyle='#332f29';
|
|
242
|
+
ctx.fillRect(bx, M.t*dpr+2, tw+12*dpr, 20*dpr); ctx.strokeRect(bx, M.t*dpr+2, tw+12*dpr, 20*dpr);
|
|
243
|
+
ctx.fillStyle='#c9c2b4'; ctx.textAlign='left'; ctx.textBaseline='middle';
|
|
244
|
+
ctx.fillText(label, bx+6*dpr, M.t*dpr+12);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function drawOverview() {
|
|
248
|
+
octx.clearRect(0,0,ov.width,ov.height);
|
|
249
|
+
octx.save(); octx.beginPath(); octx.rect(OM.l*dpr, OM.t*dpr, ovWd(), ovHt()); octx.clip();
|
|
250
|
+
incidents.forEach(iv => { const x0=ovpx(iv.a), x1=ovpx(iv.b);
|
|
251
|
+
octx.fillStyle='rgba(214,50,50,0.30)'; octx.fillRect(x0, OM.t*dpr, x1-x0, ovHt()); });
|
|
252
|
+
drawSeries(octx, ovpx, ovpy, tmin, tmax, OM.l*dpr, ovWd(), 'rgba(209,91,54,0.7)', 1.1);
|
|
253
|
+
octx.restore();
|
|
254
|
+
const vx0=ovpx(viewMin), vx1=ovpx(viewMax);
|
|
255
|
+
octx.fillStyle='rgba(27,25,22,0.55)';
|
|
256
|
+
octx.fillRect(OM.l*dpr, OM.t*dpr, vx0-OM.l*dpr, ovHt());
|
|
257
|
+
octx.fillRect(vx1, OM.t*dpr, (ov.width-OM.r*dpr)-vx1, ovHt());
|
|
258
|
+
octx.fillStyle='rgba(245,241,232,0.06)'; octx.fillRect(vx0, OM.t*dpr, vx1-vx0, ovHt());
|
|
259
|
+
octx.strokeStyle='#d15b36'; octx.lineWidth=1.5*dpr;
|
|
260
|
+
octx.strokeRect(vx0, OM.t*dpr+1, vx1-vx0, ovHt()-2);
|
|
261
|
+
octx.fillStyle='#d15b36'; const hy=OM.t*dpr+ovHt()/2-9*dpr;
|
|
262
|
+
octx.fillRect(vx0-2*dpr, hy, 4*dpr, 18*dpr); octx.fillRect(vx1-2*dpr, hy, 4*dpr, 18*dpr);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const tsAt = clientX => { const r=c.getBoundingClientRect();
|
|
266
|
+
const fr=((clientX-r.left)-M.l)/((r.width-(M.l+M.r))||1); return viewMin + clamp(fr,0,1)*vspan(); };
|
|
267
|
+
const ovTsAtCss = clientX => { const r=ov.getBoundingClientRect();
|
|
268
|
+
const fr=((clientX-r.left)-OM.l)/((r.width-(OM.l+OM.r))||1); return tmin + clamp(fr,0,1)*fullSpan; };
|
|
269
|
+
const ovEdgeCss = ts => { const r=ov.getBoundingClientRect();
|
|
270
|
+
return r.left + OM.l + (ts-tmin)/fullSpan*(r.width-(OM.l+OM.r)); };
|
|
271
|
+
|
|
272
|
+
c.addEventListener('wheel', e => { e.preventDefault(); const t=tsAt(e.clientX);
|
|
273
|
+
let s=clamp(vspan()*Math.pow(1.0015, e.deltaY), minSpan, fullSpan);
|
|
274
|
+
const f=(t-viewMin)/(vspan()||1); setView(t-f*s, t-f*s+s); }, {passive:false});
|
|
275
|
+
c.addEventListener('mousedown', e => { dragging={a:tsAt(e.clientX), b:tsAt(e.clientX), sx:e.clientX, cx:e.clientX}; });
|
|
276
|
+
c.addEventListener('mousemove', e => { if (ovAct) return;
|
|
277
|
+
if (dragging) { dragging.b=tsAt(e.clientX); dragging.cx=e.clientX; } else { hover={ts:tsAt(e.clientX)}; } draw(); });
|
|
278
|
+
c.addEventListener('mouseleave', () => { if (!dragging) { hover=null; draw(); } });
|
|
279
|
+
|
|
280
|
+
ov.addEventListener('mousedown', e => { e.preventDefault(); ov.style.cursor='grabbing';
|
|
281
|
+
const xl=ovEdgeCss(viewMin), xr=ovEdgeCss(viewMax), x=e.clientX, H=8;
|
|
282
|
+
if (Math.abs(x-xl)<=H) ovAct={type:'l'};
|
|
283
|
+
else if (Math.abs(x-xr)<=H) ovAct={type:'r'};
|
|
284
|
+
else if (x>xl && x<xr) ovAct={type:'pan', grab:ovTsAtCss(x), vMin:viewMin, vMax:viewMax};
|
|
285
|
+
else { const t=ovTsAtCss(x), s=vspan(); setView(t-s/2, t+s/2); ovAct={type:'pan', grab:t, vMin:viewMin, vMax:viewMax}; }
|
|
286
|
+
});
|
|
287
|
+
ov.addEventListener('mousemove', e => { if (ovAct) return; const x=e.clientX, H=8;
|
|
288
|
+
const xl=ovEdgeCss(viewMin), xr=ovEdgeCss(viewMax);
|
|
289
|
+
ov.style.cursor = (Math.abs(x-xl)<=H || Math.abs(x-xr)<=H) ? 'ew-resize' : (x>xl && x<xr) ? 'grab' : 'pointer'; });
|
|
290
|
+
ov.addEventListener('wheel', e => { e.preventDefault(); const t=ovTsAtCss(e.clientX);
|
|
291
|
+
const s=clamp(vspan()*Math.pow(1.0015, e.deltaY), minSpan, fullSpan); setView(t-s/2, t+s/2); }, {passive:false});
|
|
292
|
+
|
|
293
|
+
window.addEventListener('mousemove', e => { if (!ovAct) return; const t=ovTsAtCss(e.clientX);
|
|
294
|
+
if (ovAct.type==='l') setView(Math.min(t, viewMax-minSpan), viewMax);
|
|
295
|
+
else if (ovAct.type==='r') setView(viewMin, Math.max(t, viewMin+minSpan));
|
|
296
|
+
else { const d=t-ovAct.grab; setView(ovAct.vMin+d, ovAct.vMax+d); } });
|
|
297
|
+
window.addEventListener('mouseup', () => {
|
|
298
|
+
if (ovAct) { ovAct=null; return; }
|
|
299
|
+
if (!dragging) return;
|
|
300
|
+
if (Math.abs(dragging.cx-dragging.sx) > 4) {
|
|
301
|
+
const a=clamp(Math.min(dragging.a,dragging.b),tmin,tmax), b=clamp(Math.max(dragging.a,dragging.b),tmin,tmax);
|
|
302
|
+
incidents.push({a, b, label:''});
|
|
303
|
+
}
|
|
304
|
+
dragging=null; render();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
document.getElementById('zreset').onclick = () => setView(tmin, tmax);
|
|
308
|
+
c.addEventListener('dblclick', () => setView(tmin, tmax));
|
|
309
|
+
document.getElementById('clear').onclick = () => { incidents.length=0; render(); };
|
|
310
|
+
window.setLabel = (i, val) => { if (incidents[i]) incidents[i].label = val; };
|
|
311
|
+
window.rm = i => { incidents.splice(i,1); render(); };
|
|
312
|
+
|
|
313
|
+
function render() {
|
|
314
|
+
incidents.sort((p,q)=>p.a-q.a);
|
|
315
|
+
const list=document.getElementById('list');
|
|
316
|
+
list.innerHTML = incidents.map((iv,i)=>'<li><span class="dot"></span>'
|
|
317
|
+
+'<span class="span">'+fmtTs(iv.a)+' → '+fmtTs(iv.b)+'</span>'
|
|
318
|
+
+'<span class="dur">'+fmtDur(iv.b-iv.a)+'</span>'
|
|
319
|
+
+'<input class="desc" type="text" placeholder="describe this incident (optional)" '
|
|
320
|
+
+'value="'+esc(iv.label||'')+'" oninput="setLabel('+i+', this.value)">'
|
|
321
|
+
+'<button class="ghost" onclick="rm('+i+')">remove</button></li>').join('');
|
|
322
|
+
document.getElementById('empty').style.display = incidents.length ? 'none' : '';
|
|
323
|
+
const total=incidents.reduce((s,iv)=>s+(iv.b-iv.a),0);
|
|
324
|
+
document.getElementById('summary').innerHTML = incidents.length
|
|
325
|
+
? '<b>'+incidents.length+'</b> incident'+(incidents.length>1?'s':'')+' · '+fmtDur(total)+' total' : '';
|
|
326
|
+
drawAll();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
document.getElementById('export').onclick = () => {
|
|
330
|
+
const d=new Date();
|
|
331
|
+
const stamp=d.getUTCFullYear()+pad2(d.getUTCMonth()+1)+pad2(d.getUTCDate())+'T'
|
|
332
|
+
+pad2(d.getUTCHours())+pad2(d.getUTCMinutes())+pad2(d.getUTCSeconds())+'Z';
|
|
333
|
+
let y='metric: __METRIC__\\ntimezone: UTC\\nincidents:\\n';
|
|
334
|
+
const sorted=incidents.slice().sort((p,q)=>p.a-q.a);
|
|
335
|
+
if (!sorted.length) y+=' []\\n';
|
|
336
|
+
sorted.forEach(iv => { y+=' - {start: "'+fmtTs(iv.a)+'", end: "'+fmtTs(iv.b)+'"'
|
|
337
|
+
+ (iv.label && iv.label.trim() ? ', label: '+yamlStr(iv.label.trim()) : '') + '}\\n'; });
|
|
338
|
+
const blob=new Blob([y], {type:'text/yaml'}); const a=document.createElement('a');
|
|
339
|
+
a.href=URL.createObjectURL(blob); a.download='__METRIC__-'+stamp+'.yml'; a.click();
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
function drawAll() { draw(); drawOverview();
|
|
343
|
+
document.getElementById('range').textContent =
|
|
344
|
+
'viewing ' + fmtTs(viewMin) + ' → ' + fmtTs(viewMax) + ' · ' + fmtDur(vspan()) + ' of ' + fmtDur(fullSpan); }
|
|
345
|
+
function fit() { dpr = window.devicePixelRatio || 1;
|
|
346
|
+
c.width=c.clientWidth*dpr; c.height=c.clientHeight*dpr;
|
|
347
|
+
ov.width=ov.clientWidth*dpr; ov.height=ov.clientHeight*dpr; drawAll(); }
|
|
348
|
+
window.addEventListener('resize', fit); fit(); render();
|
|
349
|
+
</script>
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def render_labeler_html(metric_name: str, data: dict[str, np.ndarray]) -> str:
|
|
354
|
+
"""Return a self-contained HTML labeler page for *metric_name*'s series."""
|
|
355
|
+
timestamps = data["timestamp"]
|
|
356
|
+
values = data["value"]
|
|
357
|
+
points = []
|
|
358
|
+
for i in range(len(timestamps)):
|
|
359
|
+
v = values[i]
|
|
360
|
+
points.append({"t": _ts_to_str(timestamps[i]), "v": None if np.isnan(v) else float(v)})
|
|
361
|
+
payload = json_dumps_sorted({"metric": metric_name, "points": points})
|
|
362
|
+
return _TEMPLATE.replace("__PAYLOAD__", payload).replace("__METRIC__", metric_name)
|
|
@@ -46,12 +46,14 @@ dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
|
|
|
46
46
|
[--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run]
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
- `--incidents FILE` — a labels file (below) → **supervised** tuning.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
- `--incidents FILE|DIR` — a labels file (below) → **supervised** tuning. May be a
|
|
50
|
+
**directory** (e.g. `incidents/<name>/`) — the newest versioned file in it is
|
|
51
|
+
used. With nothing given, an interactive terminal prompts to enter incidents
|
|
52
|
+
inline; declining (or running non-interactively) tunes **unsupervised**.
|
|
53
|
+
- `--label` — write a self-contained, zoomable/pannable HTML chart of the series
|
|
54
|
+
to `metrics/<name>__labeler.html`; the user marks incidents in a browser (with
|
|
55
|
+
optional per-incident descriptions) and **Export** downloads a *versioned*
|
|
56
|
+
labels file `<name>-<UTC>.yml`. Generate-and-exit (no DB writes).
|
|
55
57
|
- `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
|
|
56
58
|
`pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
|
|
57
59
|
- `--dry-run` — run the search but persist nothing and write no config.
|
|
@@ -82,11 +84,15 @@ user to recall timestamps** — it is the easiest, most reliable path:
|
|
|
82
84
|
|
|
83
85
|
1. Run `dtk autotune --select <name> --label` (offline; writes no DB rows). It
|
|
84
86
|
renders the series to a self-contained `metrics/<name>__labeler.html`.
|
|
85
|
-
2. Tell the user to open
|
|
86
|
-
to
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
2. Tell the user to open it in a browser and mark incidents on the chart: scroll
|
|
88
|
+
to zoom, drag the navigator strip to move, **click-drag** to mark each span
|
|
89
|
+
(add an optional description), then **Export**.
|
|
90
|
+
3. Persist with versioning: a browser can't write into the project, so Export
|
|
91
|
+
downloads a versioned `<name>-<UTC>.yml`. **Move it into `incidents/<name>/`**
|
|
92
|
+
— re-labeling adds a new file, never overwrites, so every round is kept. You
|
|
93
|
+
(the assistant, with filesystem access) do this move.
|
|
94
|
+
4. Tune on the latest: point `--incidents` at the folder so the newest version is
|
|
95
|
+
used — `dtk autotune --select <name> --incidents incidents/<name>/`.
|
|
90
96
|
|
|
91
97
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
92
98
|
have exact times. If they already know the times (or you found them via a DB
|
{detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
@@ -91,11 +91,16 @@ self-contained file `metrics/<name>__labeler.html`. Walk the user through it:
|
|
|
91
91
|
|
|
92
92
|
1. Open `metrics/<name>__labeler.html` in any browser — just double-click it,
|
|
93
93
|
it's self-contained (inline chart + data, no server, no internet).
|
|
94
|
-
2.
|
|
95
|
-
|
|
96
|
-
3. Click
|
|
97
|
-
|
|
98
|
-
4.
|
|
94
|
+
2. Navigate a long/dense series: **scroll to zoom**, double-click to reset, drag
|
|
95
|
+
the **navigator strip** to move the view (window = pan, edges = stretch).
|
|
96
|
+
3. **Click-drag across the chart** over each real incident (red band + a row
|
|
97
|
+
below with an optional **description**); *remove* / *Clear all* fix mistakes.
|
|
98
|
+
4. Click **Export** — the browser downloads a *versioned* `<name>-<UTC>.yml`. A
|
|
99
|
+
browser can't write into the project, so **move it into `incidents/<name>/`**.
|
|
100
|
+
Re-labeling later adds another versioned file — nothing is overwritten, so the
|
|
101
|
+
whole history is kept. Do this move for the user (you have filesystem access).
|
|
102
|
+
5. Tune on the latest version by pointing at the folder:
|
|
103
|
+
`dtk autotune --select <name> --incidents incidents/<name>/`.
|
|
99
104
|
|
|
100
105
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
101
106
|
have exact timestamps — it is far easier than dictating times, and they label
|
|
@@ -93,6 +93,25 @@ def _resolve_scoring(scoring_override: str | None, autotune_cfg: AutoTuneConfig)
|
|
|
93
93
|
raise click.BadParameter(f"Invalid scoring metric '{value}'. Allowed: {allowed}") from exc
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
_LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _newest_labels_file(directory: Path) -> Path | None:
|
|
100
|
+
"""Newest labels file in *directory*.
|
|
101
|
+
|
|
102
|
+
The labeler exports versioned, ISO-stamped names (``<metric>-<UTC>.yml``)
|
|
103
|
+
which sort chronologically, so we pick the lexicographically-greatest name
|
|
104
|
+
(tie-broken by mtime). This lets ``--incidents incidents/<metric>/`` keep
|
|
105
|
+
every labeling round on disk while always tuning on the latest one.
|
|
106
|
+
"""
|
|
107
|
+
files: list[Path] = []
|
|
108
|
+
for pattern in _LABELS_GLOBS:
|
|
109
|
+
files.extend(directory.glob(pattern))
|
|
110
|
+
if not files:
|
|
111
|
+
return None
|
|
112
|
+
return max(files, key=lambda p: (p.name, p.stat().st_mtime))
|
|
113
|
+
|
|
114
|
+
|
|
96
115
|
def _resolve_labels(
|
|
97
116
|
*,
|
|
98
117
|
metric_name: str,
|
|
@@ -104,13 +123,24 @@ def _resolve_labels(
|
|
|
104
123
|
"""Resolve labels by precedence.
|
|
105
124
|
|
|
106
125
|
``--incidents`` flag > config ``labels_file`` > config inline ``incidents`` >
|
|
107
|
-
interactive prompt > none (unsupervised).
|
|
126
|
+
interactive prompt > none (unsupervised). A directory may be given for the
|
|
127
|
+
flag or ``labels_file``; its newest versioned file is used.
|
|
108
128
|
"""
|
|
109
129
|
path = incidents_path or autotune_cfg.labels_file
|
|
110
130
|
if path:
|
|
111
131
|
file_path = Path(path)
|
|
112
132
|
if not file_path.is_absolute():
|
|
113
133
|
file_path = project_root / file_path
|
|
134
|
+
if file_path.is_dir():
|
|
135
|
+
chosen = _newest_labels_file(file_path)
|
|
136
|
+
if chosen is None:
|
|
137
|
+
raise FileNotFoundError(
|
|
138
|
+
f"No labels files (*.yml / *.yaml / *.json) found in {file_path}"
|
|
139
|
+
)
|
|
140
|
+
labels = parse_labels_file(
|
|
141
|
+
chosen, interval_seconds=interval_seconds, metric_name=metric_name
|
|
142
|
+
)
|
|
143
|
+
return labels, f"file {chosen} (newest in {file_path}/)"
|
|
114
144
|
labels = parse_labels_file(
|
|
115
145
|
file_path, interval_seconds=interval_seconds, metric_name=metric_name
|
|
116
146
|
)
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"""Generate a self-contained HTML labeler for a metric series.
|
|
2
|
-
|
|
3
|
-
Emits a single HTML file (inline CSS/JS, no CDN) with the series embedded as a
|
|
4
|
-
JSON literal: a canvas line chart where the user click-drags over incident
|
|
5
|
-
spans and exports a labels file in the canonical schema, which is then fed back
|
|
6
|
-
via ``dtk autotune --select <metric> --incidents <file>``.
|
|
7
|
-
|
|
8
|
-
Docs sync: the autotune reference page embeds a *live* copy of this output
|
|
9
|
-
(``docs/examples/autotune-labeler.html``) so the site always shows the real UI.
|
|
10
|
-
After changing the template below, regenerate that example so the docs don't
|
|
11
|
-
drift: ``python website/scripts/gen-labeler-example.py`` (also in the release
|
|
12
|
-
checklist).
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
from datetime import datetime, timedelta
|
|
18
|
-
|
|
19
|
-
import numpy as np
|
|
20
|
-
|
|
21
|
-
from detectkit.utils.json_utils import json_dumps_sorted
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _ts_to_str(ts64: np.datetime64) -> str:
|
|
25
|
-
ms = int(ts64.astype("datetime64[ms]").astype(np.int64))
|
|
26
|
-
return (datetime(1970, 1, 1) + timedelta(milliseconds=ms)).strftime("%Y-%m-%d %H:%M:%S")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Built with .replace() (not .format()), so braces are literal — keep them single.
|
|
30
|
-
# Self-contained: inline brand styling/logo/JS, no network. Palette + fonts mirror
|
|
31
|
-
# website/src/styles/brand.css (.claude/rules/design.md); incident bands use the
|
|
32
|
-
# anomaly status color, the drag preview the no-data color.
|
|
33
|
-
_TEMPLATE = """<!doctype html>
|
|
34
|
-
<meta charset="utf-8">
|
|
35
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
-
<title>detectkit · label incidents · __METRIC__</title>
|
|
37
|
-
<style>
|
|
38
|
-
:root {
|
|
39
|
-
--clay:#d15b36; --clay-700:#b4471f; --paper:#f5f1e8; --muted:#6e675b; --faint:#9a9384;
|
|
40
|
-
--term-bg:#211e1a; --term-surface:#1b1916; --term-border:#332f29; --term-text:#c9c2b4;
|
|
41
|
-
--anomaly:#d63232; --nodata:#f0ad4e;
|
|
42
|
-
--ui:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
43
|
-
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
|
|
44
|
-
}
|
|
45
|
-
* { box-sizing: border-box; }
|
|
46
|
-
body { font-family: var(--ui); margin: 0; background: var(--term-bg); color: var(--term-text);
|
|
47
|
-
-webkit-font-smoothing: antialiased; }
|
|
48
|
-
.shell { max-width: 1080px; margin: 0 auto; padding: 22px 22px 40px; }
|
|
49
|
-
.brand { display:flex; align-items:center; gap:9px; margin-bottom: 14px; }
|
|
50
|
-
.brand svg { width: 26px; height: 26px; border-radius: 7px; display:block; }
|
|
51
|
-
.brand b { color: var(--paper); font-weight: 600; font-size: 15px; letter-spacing: .2px; }
|
|
52
|
-
.brand span { color: var(--faint); font-size: 12px; }
|
|
53
|
-
h1 { font-size: 18px; line-height: 1.3; margin: 0 0 6px; color: var(--paper); font-weight: 600; }
|
|
54
|
-
h1 code { color: var(--clay); font-family: var(--mono); font-size: .82em; }
|
|
55
|
-
.hint { color: var(--faint); font-size: 13px; margin: 0 0 18px; line-height: 1.5; }
|
|
56
|
-
.hint code { color: var(--term-text); font-family: var(--mono); font-size: 12px;
|
|
57
|
-
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 5px; padding: 1px 6px; }
|
|
58
|
-
.toolbar { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom: 12px; }
|
|
59
|
-
button { font-family: var(--ui); font-size: 13px; font-weight: 500; border: 0; border-radius: 7px;
|
|
60
|
-
padding: 9px 15px; cursor: pointer; transition: background .12s ease, border-color .12s ease; }
|
|
61
|
-
button.primary { background: var(--clay); color: #fff; }
|
|
62
|
-
button.primary:hover { background: var(--clay-700); }
|
|
63
|
-
button.ghost { background: transparent; color: var(--term-text); border: 1px solid var(--term-border); }
|
|
64
|
-
button.ghost:hover { border-color: var(--faint); color: var(--paper); }
|
|
65
|
-
.summary { margin-left: auto; color: var(--faint); font-size: 12.5px; font-family: var(--mono); }
|
|
66
|
-
.summary b { color: var(--clay); font-weight: 600; }
|
|
67
|
-
canvas { width: 100%; height: clamp(320px, 46vh, 520px); display:block;
|
|
68
|
-
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
|
|
69
|
-
.empty { color: var(--faint); font-size: 13px; margin: 16px 2px; font-style: italic; }
|
|
70
|
-
ul { list-style: none; margin: 14px 0 0; padding: 0; }
|
|
71
|
-
li { display:flex; align-items:center; gap:12px; padding: 9px 12px; font-size: 13px;
|
|
72
|
-
border: 1px solid var(--term-border); border-radius: 8px; margin-bottom: 7px; background: var(--term-surface); }
|
|
73
|
-
li .dot { width:9px; height:9px; border-radius:50%; background: var(--anomaly); flex: 0 0 auto; }
|
|
74
|
-
li .span { font-family: var(--mono); color: var(--term-text); }
|
|
75
|
-
li .dur { color: var(--faint); font-size: 12px; }
|
|
76
|
-
li button { margin-left: auto; padding: 5px 11px; font-size: 12px; }
|
|
77
|
-
footer { margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--term-border);
|
|
78
|
-
color: var(--faint); font-size: 12px; }
|
|
79
|
-
footer code { font-family: var(--mono); color: var(--term-text); }
|
|
80
|
-
</style>
|
|
81
|
-
<div class="shell">
|
|
82
|
-
<div class="brand">
|
|
83
|
-
<svg viewBox="0 0 100 100" aria-hidden="true"><rect x="3" y="3" width="94" height="94" rx="26" fill="#D15B36"/><polyline points="14,62 36,62 50,22 64,62 86,62" fill="none" stroke="#FBF9F3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="50" cy="22" r="6.5" fill="#FBF9F3"/></svg>
|
|
84
|
-
<b>detectkit</b><span>· incident labeler</span>
|
|
85
|
-
</div>
|
|
86
|
-
<h1>Label incidents — <code>__METRIC__</code></h1>
|
|
87
|
-
<p class="hint">Click-drag across the chart to mark each real incident, then <b>Export</b> and run
|
|
88
|
-
<code>dtk autotune --select __METRIC__ --incidents incidents-__METRIC__.yml</code></p>
|
|
89
|
-
<div class="toolbar">
|
|
90
|
-
<button id="export" class="primary">Export incidents-__METRIC__.yml</button>
|
|
91
|
-
<button id="clear" class="ghost">Clear all</button>
|
|
92
|
-
<span id="summary" class="summary"></span>
|
|
93
|
-
</div>
|
|
94
|
-
<canvas id="c" aria-label="metric series chart — drag horizontally to mark an incident span"></canvas>
|
|
95
|
-
<div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
|
|
96
|
-
<ul id="list"></ul>
|
|
97
|
-
<footer>All times UTC · self-contained, no data leaves your browser · generated by
|
|
98
|
-
<code>dtk autotune --label</code></footer>
|
|
99
|
-
</div>
|
|
100
|
-
<script>
|
|
101
|
-
const DATA = __PAYLOAD__;
|
|
102
|
-
const pts = DATA.points.map(p => ({t: p.t, ts: Date.parse(p.t.replace(' ','T')+'Z'), v: p.v}));
|
|
103
|
-
const incidents = [];
|
|
104
|
-
const c = document.getElementById('c');
|
|
105
|
-
const ctx = c.getContext('2d');
|
|
106
|
-
const vals = pts.map(p => p.v).filter(v => v !== null);
|
|
107
|
-
const vmin = vals.length ? Math.min.apply(null, vals) : 0;
|
|
108
|
-
const vmax = vals.length ? Math.max.apply(null, vals) : 1;
|
|
109
|
-
const tmin = pts.length ? pts[0].ts : 0;
|
|
110
|
-
const tmax = pts.length ? pts[pts.length-1].ts : 1;
|
|
111
|
-
const M = {l: 56, r: 16, t: 14, b: 30}; // css-px plot margins
|
|
112
|
-
let dpr = 1, hover = null, dragging = null;
|
|
113
|
-
function plotW() { return c.width - (M.l + M.r) * dpr; }
|
|
114
|
-
function plotH() { return c.height - (M.t + M.b) * dpr; }
|
|
115
|
-
function px(ts) { return M.l*dpr + (ts - tmin) / ((tmax - tmin) || 1) * plotW(); }
|
|
116
|
-
function py(v) { return (c.height - M.b*dpr) - (v - vmin) / ((vmax - vmin) || 1) * plotH(); }
|
|
117
|
-
function tsAt(clientX) { const r=c.getBoundingClientRect();
|
|
118
|
-
const fr=((clientX-r.left)-M.l)/((r.width-(M.l+M.r))||1); return tmin + Math.max(0,Math.min(1,fr))*(tmax-tmin); }
|
|
119
|
-
function fmtTs(ts) { return new Date(ts).toISOString().slice(0,19).replace('T',' '); }
|
|
120
|
-
function fmtAxis(ts) { return new Date(ts).toISOString().slice(5,16).replace('T',' '); }
|
|
121
|
-
function fmtVal(v) { const a=Math.abs(v); return a>=1000 ? v.toFixed(0) : a>=10 ? v.toFixed(1) : v.toFixed(2); }
|
|
122
|
-
function fmtDur(ms) { const m=Math.round(ms/60000); if(m<60) return m+'m';
|
|
123
|
-
const h=Math.floor(m/60), mm=m%60; if(h<24) return h+'h'+(mm?(' '+mm+'m'):'');
|
|
124
|
-
const d=Math.floor(h/24), hh=h%24; return d+'d'+(hh?(' '+hh+'h'):''); }
|
|
125
|
-
function fit() { dpr = window.devicePixelRatio || 1;
|
|
126
|
-
c.width = c.clientWidth * dpr; c.height = c.clientHeight * dpr; draw(); }
|
|
127
|
-
function draw() {
|
|
128
|
-
ctx.clearRect(0,0,c.width,c.height);
|
|
129
|
-
ctx.font = (11*dpr)+'px ui-sans-serif, system-ui, sans-serif';
|
|
130
|
-
// y gridlines + value ticks
|
|
131
|
-
ctx.textBaseline = 'middle';
|
|
132
|
-
for (let i=0;i<=4;i++) { const v=vmin+(vmax-vmin)*i/4, yy=py(v);
|
|
133
|
-
ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1*dpr; ctx.beginPath();
|
|
134
|
-
ctx.moveTo(M.l*dpr, yy); ctx.lineTo(c.width-M.r*dpr, yy); ctx.stroke();
|
|
135
|
-
ctx.fillStyle='#6e675b'; ctx.textAlign='right'; ctx.fillText(fmtVal(v), (M.l-8)*dpr, yy); }
|
|
136
|
-
// x ticks
|
|
137
|
-
ctx.textBaseline='top';
|
|
138
|
-
for (let i=0;i<=5;i++) { const ts=tmin+(tmax-tmin)*i/5, xx=px(ts);
|
|
139
|
-
ctx.fillStyle='#6e675b'; ctx.textAlign = i===0?'left':i===5?'right':'center';
|
|
140
|
-
ctx.fillText(fmtAxis(ts), xx, (c.height-M.b+8)*dpr); }
|
|
141
|
-
// committed incident bands
|
|
142
|
-
incidents.forEach(iv => { const x0=px(iv.a), x1=px(iv.b);
|
|
143
|
-
ctx.fillStyle='rgba(214,50,50,0.20)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH());
|
|
144
|
-
ctx.strokeStyle='rgba(214,50,50,0.55)'; ctx.lineWidth=1*dpr;
|
|
145
|
-
ctx.strokeRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); });
|
|
146
|
-
// drag preview
|
|
147
|
-
if (dragging) { const x0=px(dragging.a), x1=px(dragging.b);
|
|
148
|
-
ctx.fillStyle='rgba(240,173,78,0.28)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); }
|
|
149
|
-
// series line
|
|
150
|
-
ctx.strokeStyle='#d15b36'; ctx.lineWidth=1.5*dpr; ctx.beginPath();
|
|
151
|
-
let started=false;
|
|
152
|
-
pts.forEach(p => { if (p.v===null) { started=false; return; } const X=px(p.ts), Y=py(p.v);
|
|
153
|
-
if (!started) { ctx.moveTo(X,Y); started=true; } else ctx.lineTo(X,Y); });
|
|
154
|
-
ctx.stroke();
|
|
155
|
-
// hover crosshair + tooltip
|
|
156
|
-
if (hover && !dragging) drawHover();
|
|
157
|
-
}
|
|
158
|
-
function drawHover() {
|
|
159
|
-
let best=null, bd=Infinity;
|
|
160
|
-
for (const p of pts) { if (p.v===null) continue; const d=Math.abs(p.ts-hover.ts);
|
|
161
|
-
if (d<bd) { bd=d; best=p; } }
|
|
162
|
-
if (!best) return;
|
|
163
|
-
const X=px(best.ts), Y=py(best.v);
|
|
164
|
-
ctx.strokeStyle='rgba(201,194,180,0.25)'; ctx.lineWidth=1*dpr; ctx.beginPath();
|
|
165
|
-
ctx.moveTo(X, M.t*dpr); ctx.lineTo(X, c.height-M.b*dpr); ctx.stroke();
|
|
166
|
-
ctx.fillStyle='#f5f1e8'; ctx.beginPath(); ctx.arc(X,Y,3*dpr,0,7); ctx.fill();
|
|
167
|
-
const label=fmtAxis(best.ts)+' · '+fmtVal(best.v);
|
|
168
|
-
ctx.font=(11*dpr)+'px ui-monospace, monospace'; const tw=ctx.measureText(label).width;
|
|
169
|
-
let bx=X+8*dpr; if (bx+tw+12*dpr > c.width) bx=X-tw-20*dpr;
|
|
170
|
-
ctx.fillStyle='rgba(27,25,22,0.92)'; ctx.strokeStyle='#332f29';
|
|
171
|
-
ctx.fillRect(bx, M.t*dpr+2, tw+12*dpr, 20*dpr); ctx.strokeRect(bx, M.t*dpr+2, tw+12*dpr, 20*dpr);
|
|
172
|
-
ctx.fillStyle='#c9c2b4'; ctx.textAlign='left'; ctx.textBaseline='middle';
|
|
173
|
-
ctx.fillText(label, bx+6*dpr, M.t*dpr+12);
|
|
174
|
-
}
|
|
175
|
-
c.addEventListener('mousedown', e => { dragging={a:tsAt(e.clientX), b:tsAt(e.clientX)}; });
|
|
176
|
-
c.addEventListener('mousemove', e => { if (dragging) { dragging.b=tsAt(e.clientX); }
|
|
177
|
-
else { hover={ts:tsAt(e.clientX)}; } draw(); });
|
|
178
|
-
c.addEventListener('mouseleave', () => { hover=null; draw(); });
|
|
179
|
-
window.addEventListener('mouseup', () => { if(!dragging) return;
|
|
180
|
-
if (Math.abs(dragging.b-dragging.a) > 1000) incidents.push({a:Math.min(dragging.a,dragging.b), b:Math.max(dragging.a,dragging.b)});
|
|
181
|
-
dragging=null; render(); });
|
|
182
|
-
function render() {
|
|
183
|
-
const list=document.getElementById('list');
|
|
184
|
-
incidents.sort((p,q)=>p.a-q.a);
|
|
185
|
-
list.innerHTML = incidents.map((iv,i)=>'<li><span class="dot"></span>'
|
|
186
|
-
+'<span class="span">'+fmtTs(iv.a)+' → '+fmtTs(iv.b)+'</span>'
|
|
187
|
-
+'<span class="dur">'+fmtDur(iv.b-iv.a)+'</span>'
|
|
188
|
-
+'<button class="ghost" onclick="rm('+i+')">remove</button></li>').join('');
|
|
189
|
-
document.getElementById('empty').style.display = incidents.length ? 'none' : '';
|
|
190
|
-
const total = incidents.reduce((s,iv)=>s+(iv.b-iv.a),0);
|
|
191
|
-
document.getElementById('summary').innerHTML = incidents.length
|
|
192
|
-
? '<b>'+incidents.length+'</b> incident'+(incidents.length>1?'s':'')+' · '+fmtDur(total)+' total'
|
|
193
|
-
: '';
|
|
194
|
-
draw();
|
|
195
|
-
}
|
|
196
|
-
function rm(i) { incidents.splice(i,1); render(); }
|
|
197
|
-
document.getElementById('clear').onclick = () => { incidents.length=0; render(); };
|
|
198
|
-
document.getElementById('export').onclick = () => {
|
|
199
|
-
let y = 'metric: __METRIC__\\ntimezone: UTC\\nincidents:\\n';
|
|
200
|
-
if (!incidents.length) y += ' []\\n';
|
|
201
|
-
incidents.forEach(iv => { y += ' - {start: "'+fmtTs(iv.a)+'", end: "'+fmtTs(iv.b)+'"}\\n'; });
|
|
202
|
-
const blob = new Blob([y], {type:'text/yaml'}); const a=document.createElement('a');
|
|
203
|
-
a.href = URL.createObjectURL(blob); a.download = 'incidents-__METRIC__.yml'; a.click();
|
|
204
|
-
};
|
|
205
|
-
window.addEventListener('resize', fit); fit(); render();
|
|
206
|
-
</script>
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def render_labeler_html(metric_name: str, data: dict[str, np.ndarray]) -> str:
|
|
211
|
-
"""Return a self-contained HTML labeler page for *metric_name*'s series."""
|
|
212
|
-
timestamps = data["timestamp"]
|
|
213
|
-
values = data["value"]
|
|
214
|
-
points = []
|
|
215
|
-
for i in range(len(timestamps)):
|
|
216
|
-
v = values[i]
|
|
217
|
-
points.append({"t": _ts_to_str(timestamps[i]), "v": None if np.isnan(v) else float(v)})
|
|
218
|
-
payload = json_dumps_sorted({"metric": metric_name, "points": points})
|
|
219
|
-
return _TEMPLATE.replace("__PAYLOAD__", payload).replace("__METRIC__", metric_name)
|
|
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.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.21.0 → detectkit-0.22.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|