detectkit 0.29.0__tar.gz → 0.30.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {detectkit-0.29.0 → detectkit-0.30.1}/MANIFEST.in +1 -0
- {detectkit-0.29.0/detectkit.egg-info → detectkit-0.30.1}/PKG-INFO +1 -1
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/__init__.py +1 -1
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/html_labeler.py +67 -47
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/autotune.md +21 -2
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/cli.md +21 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/overview.md +15 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +6 -0
- detectkit-0.30.1/detectkit/cli/commands/tune.py +108 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/main.py +75 -0
- detectkit-0.30.1/detectkit/reporting/assets/report.js +77 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/builder.py +125 -1
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/html_report.py +4 -0
- detectkit-0.30.1/detectkit/tuning/__init__.py +34 -0
- detectkit-0.30.1/detectkit/tuning/assets/tune.js +51 -0
- detectkit-0.30.1/detectkit/tuning/config_writer.py +137 -0
- detectkit-0.30.1/detectkit/tuning/html.py +82 -0
- detectkit-0.30.1/detectkit/tuning/payload.py +194 -0
- detectkit-0.30.1/detectkit/tuning/server.py +179 -0
- {detectkit-0.29.0 → detectkit-0.30.1/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/SOURCES.txt +7 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/pyproject.toml +3 -0
- detectkit-0.29.0/detectkit/reporting/assets/report.js +0 -62
- {detectkit-0.29.0 → detectkit-0.30.1}/LICENSE +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/README.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/result.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/_output.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/profile.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/project_config.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/validator.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/interval.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/models.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/tables.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/stats.py +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/requirements.txt +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/setup.cfg +0 -0
- {detectkit-0.29.0 → detectkit-0.30.1}/setup.py +0 -0
|
@@ -4,6 +4,7 @@ include requirements.txt
|
|
|
4
4
|
recursive-include detectkit *.py
|
|
5
5
|
recursive-include detectkit/cli/assets *.md
|
|
6
6
|
recursive-include detectkit/reporting/assets *.js
|
|
7
|
+
recursive-include detectkit/tuning/assets *.js
|
|
7
8
|
recursive-exclude tests *
|
|
8
9
|
recursive-exclude * __pycache__
|
|
9
10
|
recursive-exclude * *.pyc
|
|
@@ -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.30.1"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -67,96 +67,116 @@ _TEMPLATE = """<!doctype html>
|
|
|
67
67
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
68
68
|
<link rel="icon" type="image/svg+xml" href="__FAVICON__">
|
|
69
69
|
<title>detectkit · label incidents · __METRIC__</title>
|
|
70
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
71
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
72
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Schibsted+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
70
73
|
<style>
|
|
71
74
|
:root {
|
|
72
|
-
--clay:#d15b36; --clay-700:#b4471f; --
|
|
75
|
+
--clay:#d15b36; --clay-700:#b4471f; --ink:#1b1916; --muted:#6e675b; --faint:#9a9384;
|
|
76
|
+
--paper:#f5f1e8; --surface:#fbf9f3; --border:#e6e0d4;
|
|
73
77
|
--term-bg:#211e1a; --term-surface:#1b1916; --term-border:#332f29; --term-text:#c9c2b4;
|
|
74
|
-
--anomaly:#d63232; --nodata:#f0ad4e;
|
|
78
|
+
--accent-green:#2e9e73; --anomaly:#d63232; --nodata:#f0ad4e;
|
|
79
|
+
--panel-shadow:0 24px 60px -30px rgba(27,25,22,.45);
|
|
75
80
|
--ui:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
76
81
|
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
|
|
77
82
|
}
|
|
78
83
|
* { box-sizing: border-box; }
|
|
79
|
-
body { font-family: var(--ui); margin: 0; background: var(--
|
|
84
|
+
body { font-family: var(--ui); margin: 0; background: var(--paper); color: var(--ink);
|
|
80
85
|
-webkit-font-smoothing: antialiased; }
|
|
81
|
-
.shell { max-width:
|
|
82
|
-
.brand { display:flex; align-items:center; gap:9px; margin-bottom:
|
|
86
|
+
.shell { max-width: 1100px; margin: 0 auto; padding: 26px 26px 48px; }
|
|
87
|
+
.brand { display:flex; align-items:center; gap:9px; margin-bottom: 18px; }
|
|
83
88
|
.brand svg { width: 26px; height: 26px; border-radius: 7px; display:block; }
|
|
84
|
-
.brand b { color: var(--
|
|
85
|
-
.brand span { color: var(--
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
.brand b { color: var(--ink); font-weight: 600; font-size: 15px; letter-spacing: .2px; }
|
|
90
|
+
.brand span { color: var(--muted); font-size: 12px; }
|
|
91
|
+
.head { display:flex; align-items:flex-start; gap:12px; margin-bottom: 14px; }
|
|
92
|
+
.head .bar { flex: 0 0 auto; width: 4px; align-self: stretch; min-height: 38px;
|
|
93
|
+
border-radius: 999px; background: var(--clay); margin-top: 2px; }
|
|
94
|
+
.head .htext { min-width: 0; }
|
|
95
|
+
h1 { font-size: 22px; line-height: 1.25; margin: 0 0 5px; color: var(--ink); font-weight: 700;
|
|
96
|
+
letter-spacing: -.01em; display:flex; align-items:center; flex-wrap:wrap; gap: 9px; }
|
|
97
|
+
h1 code { color: var(--clay); font-family: var(--mono); font-size: .7em; font-weight: 600; }
|
|
98
|
+
.subline { color: var(--muted); font-family: var(--mono); font-size: 12.5px; margin: 0; }
|
|
99
|
+
.ichip { display:inline-flex; align-items:center; gap:6px; vertical-align: middle;
|
|
100
|
+
font-family: var(--mono); font-size: 11.5px; font-weight: 500; color: var(--clay-700);
|
|
101
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 3px 11px; }
|
|
91
102
|
.ichip .d { width:6px; height:6px; border-radius:50%; background: var(--clay); }
|
|
92
|
-
.ichip b { color: var(--clay); font-weight: 700; }
|
|
93
|
-
.hint { color: var(--
|
|
94
|
-
.hint code, code.k { color: var(--
|
|
95
|
-
background: var(--
|
|
103
|
+
.ichip b { color: var(--clay-700); font-weight: 700; }
|
|
104
|
+
.hint { color: var(--muted); font-size: 13px; margin: 0 0 18px; line-height: 1.55; }
|
|
105
|
+
.hint code, code.k { color: var(--ink); font-family: var(--mono); font-size: 12px;
|
|
106
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; }
|
|
96
107
|
.toolbar { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom: 12px; }
|
|
97
108
|
button { font-family: var(--ui); font-size: 13px; font-weight: 500; border: 0; border-radius: 7px;
|
|
98
109
|
padding: 9px 15px; cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
|
|
99
110
|
button.primary { background: var(--clay); color: #fff; }
|
|
100
111
|
button.primary:hover { background: var(--clay-700); }
|
|
101
|
-
button.primary:disabled { background: var(--
|
|
102
|
-
button.ghost { background:
|
|
103
|
-
button.ghost:hover { border-color: var(--
|
|
104
|
-
button.ghost.active { border-color: var(--nodata); color: var(--
|
|
105
|
-
input.setname { background: var(--
|
|
112
|
+
button.primary:disabled { background: var(--border); color: var(--faint); cursor: default; }
|
|
113
|
+
button.ghost { background: var(--surface); color: var(--ink); border: 1px solid var(--border); }
|
|
114
|
+
button.ghost:hover { border-color: var(--clay); color: var(--clay-700); }
|
|
115
|
+
button.ghost.active { border-color: var(--nodata); color: var(--ink); background: rgba(240,173,78,0.18); }
|
|
116
|
+
input.setname { background: var(--surface); color: var(--ink); border: 1px solid var(--border);
|
|
106
117
|
border-radius: 7px; padding: 9px 11px; font-family: var(--ui); font-size: 13px; min-width: 200px; }
|
|
107
|
-
input.setname::placeholder { color: var(--
|
|
118
|
+
input.setname::placeholder { color: var(--faint); }
|
|
108
119
|
input.setname:focus { outline: none; border-color: var(--clay); }
|
|
109
|
-
.summary { margin-left: auto; color: var(--
|
|
110
|
-
.summary b { color: var(--clay); font-weight: 600; }
|
|
120
|
+
.summary { margin-left: auto; color: var(--muted); font-size: 12.5px; font-family: var(--mono); }
|
|
121
|
+
.summary b { color: var(--clay-700); font-weight: 600; }
|
|
111
122
|
.savemsg { margin: 4px 2px 0; font-size: 13px; display: none; }
|
|
112
|
-
.savemsg.ok { display: block; color: var(--accent-green
|
|
123
|
+
.savemsg.ok { display: block; color: var(--accent-green); }
|
|
113
124
|
.savemsg.err { display: block; color: var(--anomaly); }
|
|
114
|
-
.savemsg.info { display: block; color: var(--
|
|
125
|
+
.savemsg.info { display: block; color: var(--muted); }
|
|
115
126
|
.thbar { display:none; flex-wrap:wrap; gap:12px; align-items:center; margin: 0 0 12px;
|
|
116
|
-
padding: 11px 13px; border: 1px solid var(--nodata); border-radius: 9px; background: var(--
|
|
117
|
-
.thbar .thlabel { color: var(--
|
|
118
|
-
|
|
119
|
-
.thbar
|
|
127
|
+
padding: 11px 13px; border: 1px solid var(--nodata); border-radius: 9px; background: var(--surface); }
|
|
128
|
+
.thbar .thlabel { color: var(--clay-700); font-family: var(--mono); font-size: 11px; font-weight: 600;
|
|
129
|
+
letter-spacing: .06em; text-transform: uppercase; }
|
|
130
|
+
.thbar label { color: var(--muted); font-size: 12.5px; display:inline-flex; align-items:center; gap:6px; }
|
|
131
|
+
.thbar select, .thbar input { background: var(--paper); color: var(--ink); border: 1px solid var(--border);
|
|
120
132
|
border-radius: 6px; padding: 6px 8px; font-family: var(--ui); font-size: 12.5px; }
|
|
121
133
|
.thbar input.num { width: 84px; font-family: var(--mono); }
|
|
122
134
|
.thbar input:focus, .thbar select:focus { outline: none; border-color: var(--nodata); }
|
|
123
135
|
.thbar button { padding: 7px 13px; }
|
|
124
136
|
.thbar .thscope { color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
125
137
|
.thbar .thscope.hint { font-style: italic; }
|
|
126
|
-
.thbar .thscope b { color: var(--
|
|
138
|
+
.thbar .thscope b { color: var(--clay-700); font-weight: 600; font-style: normal; }
|
|
127
139
|
canvas#c { width: 100%; height: clamp(300px, 44vh, 500px); display:block; touch-action: none;
|
|
128
|
-
background: var(--term-
|
|
129
|
-
|
|
130
|
-
.
|
|
140
|
+
background: var(--term-bg); border: 1px solid var(--term-border); border-radius: 12px; cursor: crosshair;
|
|
141
|
+
box-shadow: var(--panel-shadow); }
|
|
142
|
+
.zoombar { display:flex; align-items:center; gap:8px; margin: 12px 0 6px; }
|
|
143
|
+
.rangelbl { margin-left: auto; color: var(--muted); font-size: 12px; font-family: var(--mono); }
|
|
131
144
|
canvas#ov { width: 100%; height: 66px; display:block; touch-action: none;
|
|
132
|
-
background: var(--term-
|
|
133
|
-
|
|
145
|
+
background: var(--term-bg); border: 1px solid var(--term-border); border-radius: 12px; cursor: grab;
|
|
146
|
+
box-shadow: var(--panel-shadow); }
|
|
147
|
+
.navhint { color: var(--faint); font-size: 12px; margin: 8px 2px 0; line-height: 1.55; }
|
|
134
148
|
.empty { color: var(--faint); font-size: 13px; margin: 18px 2px; font-style: italic; }
|
|
135
149
|
ul { list-style: none; margin: 16px 0 0; padding: 0; }
|
|
136
150
|
li { display:flex; align-items:center; gap:11px; padding: 9px 12px; font-size: 13px; flex-wrap: wrap;
|
|
137
|
-
border: 1px solid var(--
|
|
138
|
-
li.sel { border-color: var(--clay); background: rgba(209,91,54,0.
|
|
151
|
+
border: 1px solid var(--border); border-radius: 8px; margin-bottom: 7px; background: var(--surface); }
|
|
152
|
+
li.sel { border-color: var(--clay); background: rgba(209,91,54,0.07); }
|
|
139
153
|
li .dot { width:9px; height:9px; border-radius:50%; background: var(--anomaly); flex: 0 0 auto; }
|
|
140
|
-
li .span { font-family: var(--mono); color: var(--
|
|
141
|
-
li .dur { color: var(--
|
|
142
|
-
li input.desc { flex: 1 1 220px; min-width: 160px; background: var(--
|
|
143
|
-
border: 1px solid var(--
|
|
144
|
-
li input.desc::placeholder { color: var(--
|
|
154
|
+
li .span { font-family: var(--mono); color: var(--ink); }
|
|
155
|
+
li .dur { color: var(--muted); font-size: 12px; }
|
|
156
|
+
li input.desc { flex: 1 1 220px; min-width: 160px; background: var(--paper); color: var(--ink);
|
|
157
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; font-family: var(--ui); font-size: 12.5px; }
|
|
158
|
+
li input.desc::placeholder { color: var(--faint); }
|
|
145
159
|
li input.desc:focus { outline: none; border-color: var(--clay); }
|
|
146
160
|
li button { margin-left: auto; padding: 5px 11px; font-size: 12px; }
|
|
147
161
|
li button.focus { margin-left: auto; }
|
|
148
162
|
li button.focus + button { margin-left: 0; }
|
|
149
|
-
footer { margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--
|
|
163
|
+
footer { margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--border);
|
|
150
164
|
color: var(--faint); font-size: 12px; line-height: 1.6; }
|
|
151
|
-
footer code { font-family: var(--mono); color: var(--
|
|
165
|
+
footer code { font-family: var(--mono); color: var(--muted); }
|
|
152
166
|
</style>
|
|
153
167
|
<div class="shell">
|
|
154
168
|
<div class="brand">
|
|
155
169
|
<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>
|
|
156
170
|
<b>detectkit</b><span>· incident labeler</span>
|
|
157
171
|
</div>
|
|
158
|
-
<
|
|
159
|
-
|
|
172
|
+
<div class="head">
|
|
173
|
+
<span class="bar" aria-hidden="true"></span>
|
|
174
|
+
<div class="htext">
|
|
175
|
+
<h1>Label incidents <code>__METRIC__</code><span id="intervalchip" class="ichip"
|
|
176
|
+
title="The metric's sampling interval — the spacing between points, taken straight from the metric."></span></h1>
|
|
177
|
+
<p class="subline">incident labeler · all times UTC</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
160
180
|
<p class="hint">Click-drag across the chart to mark each real incident, add a short description, then
|
|
161
181
|
<b>Export</b>. Save the file into <code class="k">incidents/__METRIC__/</code> and run
|
|
162
182
|
<code class="k">dtk autotune --select __METRIC__ --incidents incidents/__METRIC__/</code></p>
|
|
@@ -10,6 +10,14 @@ A tuned config is an ordinary detectkit config (one chosen detector reusing the
|
|
|
10
10
|
same windowed detectors and `detector_id` identity). The fastest path is the
|
|
11
11
|
**`dtk-autotune`** skill, which runs the whole flow conversationally.
|
|
12
12
|
|
|
13
|
+
> **Prefer to tune by hand?** `dtk tune --select <metric>` is the interactive,
|
|
14
|
+
> human-in-the-loop sibling: it opens a browser view of the real series, lets you
|
|
15
|
+
> turn the knobs and watch the band recompute live, and on **Apply** writes the
|
|
16
|
+
> config back into the metric YAML **in place** (archiving the previous version to
|
|
17
|
+
> `metrics/.history/<metric>/` first). Use `autotune` to search automatically and
|
|
18
|
+
> emit a new file; use `tune` to dial a detector in by eye and commit it. See
|
|
19
|
+
> `cli.md`.
|
|
20
|
+
|
|
13
21
|
## What it searches
|
|
14
22
|
|
|
15
23
|
1. **Seasonality** — greedily builds the best `seasonality_components` grouping
|
|
@@ -57,7 +65,7 @@ ratios to choose.
|
|
|
57
65
|
|
|
58
66
|
```bash
|
|
59
67
|
dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
|
|
60
|
-
[--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run]
|
|
68
|
+
[--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run] [--report]
|
|
61
69
|
```
|
|
62
70
|
|
|
63
71
|
- `--incidents FILE|DIR` — a labels file (below) → **supervised** tuning. May be a
|
|
@@ -80,6 +88,12 @@ dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
|
|
|
80
88
|
- `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
|
|
81
89
|
`pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
|
|
82
90
|
- `--dry-run` — run the search but persist nothing and write no config.
|
|
91
|
+
- `--report [PATH]` — after tuning, emit a self-contained **HTML report** for the
|
|
92
|
+
winning config over the training window (values, confidence band, anomalies,
|
|
93
|
+
replayed alerts; offline). Bare `--report` writes
|
|
94
|
+
`reports/<name>__tuned_<id>.html`; pass a directory or a `.html` path to
|
|
95
|
+
override. `dtk run --select <m> --report` produces the same report from the
|
|
96
|
+
live config.
|
|
83
97
|
- Selectors match `dtk run`. Tuning reads loaded datapoints — if empty, run
|
|
84
98
|
`dtk run --select <m> --steps load` (optionally `--from`) first.
|
|
85
99
|
|
|
@@ -215,7 +229,12 @@ LIMIT 5
|
|
|
215
229
|
|
|
216
230
|
## Reading the tuned detector's results
|
|
217
231
|
|
|
218
|
-
|
|
232
|
+
The quickest view is an **HTML report**: add `--report` to the tune (or run
|
|
233
|
+
`dtk run --select <m> --report` later) to get a self-contained file charting the
|
|
234
|
+
winning detector's values, confidence band, flagged anomalies and the alerts it
|
|
235
|
+
would fire, with a period selector — no BI/SQL setup, offline.
|
|
236
|
+
|
|
237
|
+
To query the raw rows instead, join recent datapoints with its detections
|
|
219
238
|
(`value` vs `confidence_lower/upper` vs `is_anomaly`) for the
|
|
220
239
|
`winning_detector_id` — see the per-backend query templates in the
|
|
221
240
|
**`dtk-autotune`** skill and in the visualizing-results guide.
|
|
@@ -11,6 +11,7 @@ Run all commands from a project directory (the one containing
|
|
|
11
11
|
| `dtk init-claude` | (Re)generate this Claude context (CLAUDE.md + `.claude/rules/detectkit/` + skills) |
|
|
12
12
|
| `dtk run --select <sel>` | Run the load → detect → alert pipeline |
|
|
13
13
|
| `dtk autotune --select <sel>` | Auto-configure a metric's detector (see `autotune.md`) |
|
|
14
|
+
| `dtk tune --select <sel>` | Interactively tune a detector on real data, write it back in place |
|
|
14
15
|
| `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
|
|
15
16
|
| `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
|
|
16
17
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
@@ -72,6 +73,26 @@ self-contained HTML report as `dtk run` for the tuned winner (default
|
|
|
72
73
|
`reports/<metric>__tuned_<id>.html`; `<dir>` or a `.html` file also accepted).
|
|
73
74
|
Full reference: `autotune.md`.
|
|
74
75
|
|
|
76
|
+
## `dtk tune --select <sel>`
|
|
77
|
+
|
|
78
|
+
The **manual, interactive** sibling of `dtk autotune`. Opens a localhost browser
|
|
79
|
+
view of the metric's **real** persisted series and lets you turn the detector's
|
|
80
|
+
knobs (type, threshold, window, recency weighting + half-life, detrend, smoothing,
|
|
81
|
+
seasonality conditioning, alert `consecutive_anomalies`) while the confidence band
|
|
82
|
+
and flagged anomalies **recompute live**. Clicking **Apply** writes the chosen
|
|
83
|
+
config back into the metric YAML **in place** (autotune, by contrast, writes a new
|
|
84
|
+
`__tuned_<id>.yml` and never edits the original). Reads the metric's loaded
|
|
85
|
+
datapoints (run `dtk run --steps load` first if empty); the selector must resolve
|
|
86
|
+
to a single metric.
|
|
87
|
+
|
|
88
|
+
Safe write-back: the config is validated before anything is written, the previous
|
|
89
|
+
YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
|
|
90
|
+
overwritten. Takes **no pipeline lock** (it only edits a config file); re-run
|
|
91
|
+
`dtk run` afterward to recompute detections under the new config.
|
|
92
|
+
`--no-serve` writes a static read-only preview HTML instead (no write-back);
|
|
93
|
+
`--from` / `--to` bound the window; `--no-open` prints the URL without opening a
|
|
94
|
+
browser.
|
|
95
|
+
|
|
75
96
|
## `dtk test-alert <metric>`
|
|
76
97
|
|
|
77
98
|
Sends a mock alert (fake value/CI/severity) through the metric's configured
|
|
@@ -82,6 +82,21 @@ detectors agreeing under a `direction` policy) that must hold for
|
|
|
82
82
|
and the rule it fired on, with the anomaly shown as evidence. See
|
|
83
83
|
`alerting.md`.
|
|
84
84
|
|
|
85
|
+
## Seeing results — HTML reports
|
|
86
|
+
|
|
87
|
+
Beyond alerts, `dtk run --select <m> --report` (and `dtk autotune --report`)
|
|
88
|
+
writes a **self-contained HTML report**: the metric's values, each detector's
|
|
89
|
+
confidence band, flagged anomalies, and the alerts/recoveries/no-data it fired,
|
|
90
|
+
over a selectable period — so you can see how a metric behaved without standing
|
|
91
|
+
up BI or a SQL dashboard. Offline, nothing leaves the browser. Bare `--report`
|
|
92
|
+
writes `reports/<metric>.html`; pass a directory or a `.html` path to override.
|
|
93
|
+
See `cli.md`.
|
|
94
|
+
|
|
95
|
+
The report is read-only. To **change** the detector — turn its knobs on the real
|
|
96
|
+
series, watch the band recompute live, then write the config back into the metric
|
|
97
|
+
— use `dtk tune --select <m>`, the interactive sibling of `dtk autotune`
|
|
98
|
+
(`cli.md`).
|
|
99
|
+
|
|
85
100
|
## Glossary
|
|
86
101
|
|
|
87
102
|
- **metric** — a named time series (SQL + interval) you monitor.
|
{detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
@@ -187,6 +187,12 @@ later for a sharper tune.
|
|
|
187
187
|
|
|
188
188
|
## Step 4 — Study and present the result
|
|
189
189
|
|
|
190
|
+
For a visual the user can open, re-run the tune with `--report` (or run
|
|
191
|
+
`dtk run --select <name>__tuned_<id> --report`): it writes a self-contained
|
|
192
|
+
`reports/<name>__tuned_<id>.html` charting values, the confidence band, anomalies
|
|
193
|
+
and the alerts that would fire — no SQL needed. The raw-row queries below remain
|
|
194
|
+
the fallback for inline inspection.
|
|
195
|
+
|
|
190
196
|
Read the emitted `metrics/<name>__tuned_<id>.yml` (do not re-run the search).
|
|
191
197
|
The `#` comment header walks the whole decision; summarize for the user:
|
|
192
198
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""``dtk tune`` — interactive manual tuning of a metric's detector.
|
|
2
|
+
|
|
3
|
+
The human-in-the-loop sibling of ``dtk autotune``: load the metric's persisted
|
|
4
|
+
datapoints, open an interactive browser view of the **real** series, let the
|
|
5
|
+
user turn the detector's knobs and watch the band recompute live, then write the
|
|
6
|
+
chosen config back into the metric YAML (archiving the previous version first).
|
|
7
|
+
|
|
8
|
+
Unlike ``run``/``autotune`` it takes **no pipeline lock** — it neither runs the
|
|
9
|
+
pipeline nor persists detections; it only edits a config file (like a human
|
|
10
|
+
editing YAML). Re-run ``dtk run`` afterwards to recompute detections under the
|
|
11
|
+
new config.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from detectkit.cli._output import echo_done, echo_error, echo_noop
|
|
19
|
+
from detectkit.cli.commands.autotune import _load_project
|
|
20
|
+
from detectkit.cli.commands.run import parse_date, select_metrics
|
|
21
|
+
from detectkit.tuning import build_tune_payload, render_tune_html, serve_tuner
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_tune(
|
|
25
|
+
*,
|
|
26
|
+
select: str,
|
|
27
|
+
profile: str | None = None,
|
|
28
|
+
from_date: str | None = None,
|
|
29
|
+
to_date: str | None = None,
|
|
30
|
+
no_serve: bool = False,
|
|
31
|
+
no_open: bool = False,
|
|
32
|
+
) -> bool:
|
|
33
|
+
"""Run the interactive tuner for the single selected metric.
|
|
34
|
+
|
|
35
|
+
Returns ``True`` when a config was written (or a static preview emitted),
|
|
36
|
+
``False`` on a no-op / error / cancellation.
|
|
37
|
+
"""
|
|
38
|
+
loaded = _load_project(profile)
|
|
39
|
+
if loaded is None:
|
|
40
|
+
return False
|
|
41
|
+
project_root, project_config, internal_manager, _db_manager = loaded
|
|
42
|
+
project_name = getattr(project_config, "name", None)
|
|
43
|
+
|
|
44
|
+
metrics = select_metrics(select, project_root)
|
|
45
|
+
if not metrics:
|
|
46
|
+
echo_error(select, "no metrics matched the selector")
|
|
47
|
+
return False
|
|
48
|
+
if len(metrics) > 1:
|
|
49
|
+
names = ", ".join(c.name for _p, c in metrics)
|
|
50
|
+
echo_error(
|
|
51
|
+
select,
|
|
52
|
+
f"matched {len(metrics)} metrics ({names}); `dtk tune` tunes one at a time — "
|
|
53
|
+
"narrow --select to a single metric",
|
|
54
|
+
)
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
metric_path, config = metrics[0]
|
|
58
|
+
name = config.name
|
|
59
|
+
|
|
60
|
+
from_dt = parse_date(from_date) if from_date else None
|
|
61
|
+
to_dt = parse_date(to_date) if to_date else None
|
|
62
|
+
# The builder resolves the window itself (recent ~TUNE_DEFAULT_POINTS by
|
|
63
|
+
# default, or the explicit --from/--to span) and reads only that slice — no
|
|
64
|
+
# need to pull the whole history just to find the bounds.
|
|
65
|
+
payload = build_tune_payload(
|
|
66
|
+
metric_config=config,
|
|
67
|
+
internal=internal_manager,
|
|
68
|
+
start=from_dt,
|
|
69
|
+
end=to_dt,
|
|
70
|
+
project_name=project_name,
|
|
71
|
+
)
|
|
72
|
+
n_points = len(payload["points"])
|
|
73
|
+
if n_points == 0:
|
|
74
|
+
echo_noop(
|
|
75
|
+
name,
|
|
76
|
+
"no datapoints in range — run `dtk run --select <name> --steps load` first, "
|
|
77
|
+
"or widen --from/--to",
|
|
78
|
+
)
|
|
79
|
+
return False
|
|
80
|
+
span = "the most recent points" if not (from_date or to_date) else "the selected window"
|
|
81
|
+
click.echo(f" Tuning on {n_points} points ({span}; pass --from/--to for a different span).")
|
|
82
|
+
|
|
83
|
+
# Static, read-only preview (no localhost server, no write-back).
|
|
84
|
+
if no_serve:
|
|
85
|
+
out = project_root / "metrics" / f"{name}__tuner.html"
|
|
86
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
out.write_text(render_tune_html(payload), encoding="utf-8")
|
|
88
|
+
echo_done(
|
|
89
|
+
f"{name}: wrote static tuner preview {out.relative_to(project_root)} "
|
|
90
|
+
"(read-only — sliders recompute live, but no Apply)"
|
|
91
|
+
)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
applied = serve_tuner(
|
|
95
|
+
payload=payload,
|
|
96
|
+
original_path=metric_path,
|
|
97
|
+
project_root=project_root,
|
|
98
|
+
open_browser=not no_open,
|
|
99
|
+
echo=click.echo,
|
|
100
|
+
)
|
|
101
|
+
if applied is None:
|
|
102
|
+
echo_noop(name, "tuning cancelled — metric unchanged")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
click.echo(f" Archived previous config: {applied.archived.relative_to(project_root)}")
|
|
106
|
+
echo_done(f"{name}: applied tuned detector → {applied.saved.relative_to(project_root)}")
|
|
107
|
+
click.echo(f" Re-run `dtk run --select {name}` to recompute detections under the new config.")
|
|
108
|
+
return True
|
|
@@ -329,6 +329,81 @@ def autotune(
|
|
|
329
329
|
)
|
|
330
330
|
|
|
331
331
|
|
|
332
|
+
@cli.command()
|
|
333
|
+
@click.option(
|
|
334
|
+
"--select",
|
|
335
|
+
"-s",
|
|
336
|
+
help="Selector for the single metric to tune (metric name, path, or tag)",
|
|
337
|
+
required=True,
|
|
338
|
+
)
|
|
339
|
+
@click.option(
|
|
340
|
+
"--from",
|
|
341
|
+
"from_date",
|
|
342
|
+
help="Window start (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)",
|
|
343
|
+
)
|
|
344
|
+
@click.option(
|
|
345
|
+
"--to",
|
|
346
|
+
"to_date",
|
|
347
|
+
help="Window end (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)",
|
|
348
|
+
)
|
|
349
|
+
@click.option(
|
|
350
|
+
"--profile",
|
|
351
|
+
help="Profile to use (default: from project config)",
|
|
352
|
+
)
|
|
353
|
+
@click.option(
|
|
354
|
+
"--no-serve",
|
|
355
|
+
is_flag=True,
|
|
356
|
+
help="Write a static, read-only tuner HTML file and exit instead of serving (no write-back)",
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--no-open",
|
|
360
|
+
is_flag=True,
|
|
361
|
+
help="Don't auto-open the browser (just print the local URL)",
|
|
362
|
+
)
|
|
363
|
+
def tune(
|
|
364
|
+
select: str,
|
|
365
|
+
from_date: str,
|
|
366
|
+
to_date: str,
|
|
367
|
+
profile: str,
|
|
368
|
+
no_serve: bool,
|
|
369
|
+
no_open: bool,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Interactively tune a metric's detector on its real data, then write it back.
|
|
373
|
+
|
|
374
|
+
The manual, human-in-the-loop sibling of `dtk autotune`: opens an interactive
|
|
375
|
+
browser view of the metric's persisted series, lets you turn the detector's
|
|
376
|
+
knobs (type, threshold, window, weighting, detrend, smoothing, seasonality)
|
|
377
|
+
and watch the confidence band + flagged anomalies recompute live, then — on a
|
|
378
|
+
click — writes the chosen config back into the metric YAML.
|
|
379
|
+
|
|
380
|
+
Safe by construction: the new config is validated before anything is written,
|
|
381
|
+
the previous metric YAML is archived under metrics/.history/<metric>/, and
|
|
382
|
+
only then is the metric overwritten. Takes no pipeline lock (it only edits a
|
|
383
|
+
config file); re-run `dtk run` afterwards to recompute detections.
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
# Tune interactively and apply on click
|
|
387
|
+
dtk tune --select checkout_errors
|
|
388
|
+
|
|
389
|
+
# Tune over a specific window
|
|
390
|
+
dtk tune --select checkout_errors --from 2026-05-01 --to 2026-06-01
|
|
391
|
+
|
|
392
|
+
# Write a static, read-only preview file (no write-back)
|
|
393
|
+
dtk tune --select checkout_errors --no-serve
|
|
394
|
+
"""
|
|
395
|
+
from detectkit.cli.commands.tune import run_tune
|
|
396
|
+
|
|
397
|
+
run_tune(
|
|
398
|
+
select=select,
|
|
399
|
+
profile=profile,
|
|
400
|
+
from_date=from_date,
|
|
401
|
+
to_date=to_date,
|
|
402
|
+
no_serve=no_serve,
|
|
403
|
+
no_open=no_open,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
332
407
|
@cli.command()
|
|
333
408
|
@click.argument("metric_name")
|
|
334
409
|
@click.option(
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";(()=>{var ke={"--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 T(e){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||ke[e]||"#888"}function ge(e){let t=e.replace("#","").trim();t.length===3&&(t=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]);let n=parseInt(t,16);return t.length!==6||Number.isNaN(n)?[209,91,54]:[n>>16&255,n>>8&255,n&255]}function _(e,t){let[n,i,s]=ge(e);return`rgba(${n},${i},${s},${t})`}function Z(e){let t=Math.max(1,window.devicePixelRatio||1),n=e.clientWidth||e.offsetWidth||0,i=e.clientHeight||e.offsetHeight||0;return e.width=Math.round(n*t),e.height=Math.round(i*t),t}function ee(e,t,n,i){let s=()=>e.width-(t.l+t.r)*i,v=()=>e.height-(t.t+t.b)*i,o=()=>n.tmax-n.tmin||1,a=()=>n.vmax-n.vmin||1;return{px:c=>t.l*i+(c-n.tmin)/o()*s(),py:c=>e.height-t.b*i-(c-n.vmin)/a()*v(),tAt:c=>n.tmin+(c-t.l*i)/(s()||1)*o(),vAt:c=>n.vmin+(e.height-t.b*i-c)/(v()||1)*a(),plotW:s,plotH:v,tspan:o}}var B=Number.isFinite;function te(e,t,n,i,s,v,o,a,p,m,b,u){let c=t.length,A=Math.max(1,Math.round(o)),E=s-i||1,x=0;for(let h=0;h<c;h++){let f=n[h];!B(f)||t[h]<i||t[h]>s||x++}if(e.strokeStyle=m,e.lineWidth=b*u,e.lineJoin="round",e.beginPath(),x<=A){let h=!1;for(let f=0;f<c;f++){let L=n[f],S=t[f];if(!B(L)||S<i||S>s){h=!1;continue}let $=a(S),C=p(L);h?e.lineTo($,C):(e.moveTo($,C),h=!0)}}else{let h=new Array(A).fill(null),f=new Array(A).fill(null);for(let S=0;S<c;S++){let $=n[S],C=t[S];if(!B($)||C<i||C>s)continue;let P=Math.floor((C-i)/E*(A-1));P=P<0?0:P>A-1?A-1:P,(h[P]===null||$<h[P])&&(h[P]=$),(f[P]===null||$>f[P])&&(f[P]=$)}let L=!1;for(let S=0;S<A;S++){if(f[S]===null){L=!1;continue}let $=v+S,C=p(f[S]),P=p(h[S]);L?e.lineTo($,C):(e.moveTo($,C),L=!0),e.lineTo($,P)}}e.stroke()}function ne(e){let t=[],n=-1;for(let i=0;i<e.length;i++){let s=e[i];s.lo!==null&&s.hi!==null&&B(s.lo)&&B(s.hi)?n===-1&&(n=i):n!==-1&&(t.push([n,i-1]),n=-1)}return n!==-1&&t.push([n,e.length-1]),t}function re(e,t,n,i,s,v,o,a,p){e.fillStyle=_(v,o);for(let[m,b]of n){e.beginPath(),e.moveTo(i(t[m].t),s(t[m].hi));for(let u=m+1;u<=b;u++)e.lineTo(i(t[u].t),s(t[u].hi));for(let u=b;u>=m;u--)e.lineTo(i(t[u].t),s(t[u].lo));e.closePath(),e.fill()}e.strokeStyle=_(v,a),e.lineWidth=1*p;for(let[m,b]of n)for(let u of["hi","lo"]){e.beginPath();for(let c=m;c<=b;c++){let A=i(t[c].t),E=s(t[c][u]);c===m?e.moveTo(A,E):e.lineTo(A,E)}e.stroke()}}function oe(e,t,n,i,s,v,o,a){for(let p=0;p<t.length;p++){let m=t[p];if(!B(m.v)||m.t<n||m.t>i)continue;let b=s(m.t),u=v(m.v);e.fillStyle=_(o,.18),e.beginPath(),e.arc(b,u,6*a,0,Math.PI*2),e.fill(),e.fillStyle=o,e.beginPath(),e.arc(b,u,3*a,0,Math.PI*2),e.fill()}}function ie(e,t,n,i,s,v,o,a,p,m,b){e.font=`${11*b}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let c=0;c<=4;c++){let A=i.vmin+(i.vmax-i.vmin)*c/4,E=v(A);e.strokeStyle=_(p,.1),e.lineWidth=1*b,e.beginPath(),e.moveTo(n.l*b,E),e.lineTo(t.width-n.r*b,E),e.stroke(),e.fillStyle=m,e.textAlign="right",e.fillText(W(A),(n.l-8)*b,E)}e.textBaseline="top";let u=a-o||1;for(let c=0;c<=5;c++){let A=o+u*c/5,E=s(A);e.fillStyle=m,e.textAlign=c===0?"left":c===5?"right":"center",e.fillText(we(A,u),E,(t.height-n.b+7)*b)}}function W(e){let t=Math.abs(e);return t>=1e3?e.toFixed(0):t>=10?e.toFixed(1):t>=1?e.toFixed(2):e.toFixed(3)}function we(e,t){let n=new Date(e).toISOString();return t<2*864e5?n.slice(5,16).replace("T"," "):n.slice(5,10)}function I(e){return new Date(e).toISOString().slice(0,19).replace("T"," ")}function se(e){let t=Math.round(e/6e4);if(t<60)return t+"m";let n=Math.floor(t/60),i=t%60;if(n<24)return n+"h"+(i?" "+i+"m":"");let s=Math.floor(n/24),v=n%24;return s+"d"+(v?" "+v+"h":"")}function ae(e,t,n){return{left:t.l*n,top:t.t*n,right:e.width-t.r*n,bottom:e.height-t.b*n}}function le(e,t,n,i,s,v,o){let a=ae(t,n,i),p=Math.max(a.left,Math.min(s(v),a.right));p<=a.left+.5||(e.save(),e.fillStyle="rgba(17,15,13,0.42)",e.fillRect(a.left,a.top,p-a.left,a.bottom-a.top),e.strokeStyle=_(T("--faint"),.7),e.lineWidth=1*i,e.setLineDash([4*i,4*i]),e.beginPath(),e.moveTo(p,a.top),e.lineTo(p,a.bottom),e.stroke(),e.setLineDash([]),e.fillStyle=_(T("--faint"),.95),e.font=`${10*i}px ui-monospace, monospace`,e.textAlign="left",e.textBaseline="top",e.fillText(o,p+6*i,a.top+5*i),e.restore())}function ce(e,t,n,i,s,v,o){let a=ae(t,n,i),p=5*i;e.save();for(let m=0;m<v.length;m++){let b=v[m],u=s(b.t);if(u<a.left-1||u>a.right+1)continue;let c=o(b.kind);e.strokeStyle=_(c,.45),e.lineWidth=1*i,e.beginPath(),e.moveTo(u,a.top),e.lineTo(u,a.bottom),e.stroke(),e.fillStyle=c,e.beginPath(),e.moveTo(u-p,a.top),e.lineTo(u+p,a.top),e.lineTo(u,a.top+p*1.4),e.closePath(),e.fill()}e.restore()}var D={l:56,r:16,t:14,b:28},ye=300*1e3,d="dtk-report";function q(e){return e==="recovery"?T("--st-recovery"):e==="no_data"?T("--st-nodata"):T("--st-anomaly")}function $e(e){return e==="recovery"?"recovery":e==="no_data"?"no-data":"anomaly"}var y=e=>String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),O=(e,t,n)=>Math.max(t,Math.min(n,e)),de=["--clay","--st-error","--st-recovery","--st-nodata"];function Me(e,t){Le(),t.classList.add(d),t.innerHTML="";let n=document.createElement("div");n.className="dtk-report-root",t.appendChild(n);let i=new Map;for(let x of e.points)x.v!==null&&i.set(x.t,x.v);let s=e.detectors.map((x,h)=>{let f=x.effective_start,L=x.points.map($=>f!==null&&$.t<f?{t:$.t,lo:null,hi:null}:{t:$.t,lo:$.lo,hi:$.hi}),S=[];for(let $ of x.points)if($.a===1&&(f===null||$.t>=f)){let C=i.get($.t);C!==void 0&&S.push({t:$.t,v:C})}return{det:x,band:L,anomalies:S,effectiveStart:f,color:T(de[h%de.length]),shown:h===0}});s.length===1&&(s[0].shown=!0),n.appendChild(Ae(e));let v=null;s.length>1&&(v=Se(s,()=>u.repaint()),n.appendChild(v));let o=document.createElement("div");o.className="dtk-bar",n.appendChild(o);let a=document.createElement("div");a.className="dtk-presets",o.appendChild(a);let p=document.createElement("div");p.className="dtk-readout",p.textContent="hover the chart for a point readout",o.appendChild(p);let m=document.createElement("div");m.className="dtk-chart";let b=document.createElement("canvas");m.appendChild(b),n.appendChild(m);let u=Ce(b,e,s,i,x=>{p.innerHTML=x}),c=[{label:"24h",ms:24*3600*1e3},{label:"7d",ms:7*86400*1e3},{label:"30d",ms:30*86400*1e3},{label:"All",ms:null}];for(let x of c){let h=document.createElement("button");h.className="dtk-preset",h.textContent=x.label,h.onclick=()=>{x.ms===null?u.resetView():u.setView(e.period.end-x.ms,e.period.end),Ee(a,h)},a.appendChild(h)}n.appendChild(Te(e,x=>u.focusAlert(x))),u.resize();let A=0,E=()=>{A||(A=requestAnimationFrame(()=>{A=0,u.resize()}))};window.addEventListener("resize",E)}function Ae(e){let t=document.createElement("div");t.className="dtk-header";let n=e.interval_seconds/60,i=e.interval_seconds>=86400?e.interval_seconds/86400+"d":e.interval_seconds>=3600?e.interval_seconds/3600+"h":n>=1?n+"min":e.interval_seconds+"s",s=e.project?`${y(e.project)} \xB7 ${y(e.metric)}`:y(e.metric),v=e.summary;return t.innerHTML=`<div class="dtk-h-top"><h1 class="dtk-title">${s}</h1><div class="dtk-meta">${y(I(e.period.start))} \u2013 ${y(I(e.period.end))} \xB7 interval ${y(i)}${e.generated_at?` \xB7 generated ${y(e.generated_at)}`:""}</div></div>`+(e.description?`<p class="dtk-desc">${y(e.description)}</p>`:"")+'<div class="dtk-chips">'+K("anomalies",v.anomalies,"--st-anomaly")+K("alerts",v.alerts,"--clay")+K("recoveries",v.recoveries,"--st-recovery")+K("no-data",v.no_data,"--st-nodata")+"</div>",t}function K(e,t,n){let i=T(n);return`<span class="dtk-chip"><span class="dtk-dot" style="background:${y(i)}"></span><span class="dtk-chip-n">${t}</span><span class="dtk-chip-l">${y(e)}</span></span>`}function Se(e,t){let n=document.createElement("div");return n.className="dtk-legend",e.forEach(i=>{let s=document.createElement("button");s.className="dtk-legend-item"+(i.shown?"":" off"),s.innerHTML=`<span class="dtk-swatch" style="background:${y(i.color)}"></span><span class="dtk-legend-name">${y(i.det.name)}</span><span class="dtk-legend-id">${y(i.det.id.slice(0,8))}</span><span class="dtk-legend-n">${i.det.anomaly_count}</span>`,s.onclick=()=>{i.shown=!i.shown,s.classList.toggle("off",!i.shown),t()},n.appendChild(s)}),n}function Te(e,t){let n=document.createElement("div");n.className="dtk-alerts";let i=document.createElement("div");if(i.className="dtk-alerts-head",i.textContent=`Alerts (${e.alerts.length})`,n.appendChild(i),e.alerts.length===0){let o=document.createElement("div");return o.className="dtk-alerts-empty",o.textContent="No alerts fired in this period.",n.appendChild(o),n}let s=document.createElement("div");s.className="dtk-alerts-list";let v=[...e.alerts].sort((o,a)=>a.t-o.t);for(let o of v){let a=document.createElement("button");a.className="dtk-alert-row";let p=q(o.kind),m=o.direction!=="none"?` \xB7 ${y(o.direction)}`:"",b=o.severity>0?` \xB7 sev ${o.severity.toFixed(2)}`:"",u=o.value!==null?` \xB7 value ${W(o.value)}`:"",c=o.onset!==null&&o.kind!=="no_data"?` \xB7 ${se(Math.max(0,o.t-o.onset))} (${o.consecutive} pts)`:"";a.innerHTML=`<span class="dtk-alert-time">${y(I(o.t))}</span><span class="dtk-badge" style="background:${y(_(p,.18))};color:${y(p)};border-color:${y(_(p,.5))}">${y($e(o.kind))}</span><span class="dtk-alert-body"><span class="dtk-alert-rule">${y(o.rule)}</span><span class="dtk-alert-sub">${y(o.detector)}${m}${b}${u}${y(c)}</span></span>`,a.onclick=()=>t(o),s.appendChild(a)}return n.appendChild(s),n}function Ee(e,t){e.querySelectorAll(".dtk-preset").forEach(n=>n.classList.remove("active")),t.classList.add("active")}function Ce(e,t,n,i,s){let v=e.getContext("2d");if(!v)throw new Error("report: 2D context unavailable");let o=v,a=t.points.map(r=>r.t),p=t.points.map(r=>r.v===null?NaN:r.v),m=t.period.start,b=t.period.end,u=b-m||1,c=Math.min(ye,u),A=0,E=1;S();let x=m,h=b,f=1,L=null;function S(){let r=1/0,l=-1/0;for(let w of p)Number.isFinite(w)&&(w<r&&(r=w),w>l&&(l=w));for(let w of n)for(let g of w.band)g.lo!==null&&Number.isFinite(g.lo)&&g.lo<r&&(r=g.lo),g.hi!==null&&Number.isFinite(g.hi)&&g.hi>l&&(l=g.hi);(!Number.isFinite(r)||!Number.isFinite(l))&&(r=0,l=1),l<=r&&(l=r+1);let k=(l-r)*.06;A=r-k,E=l+k}function $(){for(let r of n)if(r.shown&&r.effectiveStart!==null)return r.effectiveStart;for(let r of n)if(r.effectiveStart!==null)return r.effectiveStart;return null}function C(){return{tmin:x,tmax:h,vmin:A,vmax:E}}function P(){return ee(e,D,C(),f)}function V(r,l){let k=l-r;if(k<c){let w=(r+l)/2;r=w-c/2,l=w+c/2,k=c}k>=u&&(r=m,l=b),r<m&&(l+=m-r,r=m),l>b&&(r-=l-b,l=b),x=O(r,m,b),h=O(l,m,b),X()}function G(){x=m,h=b,X()}function me(r){let l=r.onset!==null?Math.min(r.onset,r.t):r.t,k=r.t,g=Math.max(k-l,c)*1.5+c;V(l-g,k+g)}let Y=0;function j(){Y===0&&(Y=requestAnimationFrame(X))}function X(){if(Y=0,e.width===0||e.height===0)return;let r=P(),l=T("--faint"),k=T("--muted"),w=T("--clay");if(o.fillStyle=T("--term-bg"),o.fillRect(0,0,e.width,e.height),a.length===0)return;ie(o,e,D,C(),r.px,r.py,x,h,l,k,f),o.save(),o.beginPath(),o.rect(D.l*f,D.t*f,r.plotW(),r.plotH()),o.clip();let g=D.t*f,N=r.plotH();for(let M of t.alerts){if(M.onset===null||M.kind==="no_data")continue;let F=Math.min(M.onset,M.t),U=Math.max(M.onset,M.t);if(U<x||F>h)continue;let ve=q(M.kind),Q=r.px(F),xe=r.px(U);o.fillStyle=_(ve,.08),o.fillRect(Q,g,Math.max(xe-Q,1*f),N)}for(let M of n){if(!M.shown)continue;let F=ne(M.band);re(o,M.band,F,r.px,r.py,M.color,.13,.4,f)}te(o,a,p,x,h,D.l*f,r.plotW(),r.px,r.py,w,1.5,f);for(let M of n)M.shown&&oe(o,M.anomalies,x,h,r.px,r.py,T("--st-anomaly"),f);let R=$();R!==null&&R>x&&le(o,e,D,f,r.px,R,"detection at full power \u2192");let z=t.alerts.map(M=>({t:M.t,kind:M.kind}));ce(o,e,D,f,r.px,z,M=>q(M)),L!==null&&pe(r,g,N,l),o.restore()}function fe(r){let l=a;if(l.length===0)return-1;let k=0,w=l.length-1;for(;k<w;){let g=k+w>>1;l[g]<r?k=g+1:w=g}return k>0&&r-l[k-1]<l[k]-r&&(k-=1),k}function pe(r,l,k,w){let g=fe(L);if(g<0)return;let N=a[g];if(N<x||N>h){s("hover the chart for a point readout");return}let R=r.px(N);o.strokeStyle=_(w,.45),o.lineWidth=1*f,o.setLineDash([2*f,2*f]),o.beginPath(),o.moveTo(R,l),o.lineTo(R,l+k),o.stroke(),o.setLineDash([]);let z=p[g];if(Number.isFinite(z)){let M=r.py(z);o.fillStyle=T("--term-bg"),o.beginPath(),o.arc(R,M,4*f,0,Math.PI*2),o.fill(),o.strokeStyle=T("--clay"),o.lineWidth=2*f,o.beginPath(),o.arc(R,M,4*f,0,Math.PI*2),o.stroke()}be(g)}function be(r){let l=a[r],k=p[r],w=`<span class="dtk-ro-t">${y(I(l))}</span>`;w+=`<span class="dtk-ro-v">value ${Number.isFinite(k)?W(k):"\u2014"}</span>`;for(let g of n){if(!g.shown)continue;let N=g.band[r],R=g.det.points[r];if(N&&N.lo!==null&&N.hi!==null){let z=R&&R.a===1,M=z&&R.sev!==null?` sev ${R.sev.toFixed(2)}`:"",F=z?`<span class="dtk-ro-anom" style="color:${y(T("--st-anomaly"))}">anomaly${y(M)}</span>`:'<span class="dtk-ro-ok">ok</span>';w+=`<span class="dtk-ro-det"><span class="dtk-swatch" style="background:${y(g.color)}"></span>${y(g.det.name)}: [${W(N.lo)}, ${W(N.hi)}] ${F}</span>`}}s(w)}function J(r){let l=e.getBoundingClientRect(),k=(r-l.left-D.l)/(l.width-(D.l+D.r)||1);return x+O(k,0,1)*(h-x)}e.addEventListener("wheel",r=>{r.preventDefault();let l=J(r.clientX),k=h-x,w=O(k*Math.pow(1.0015,r.deltaY),c,u),g=(l-x)/(k||1);V(l-g*w,l-g*w+w)},{passive:!1});let H=null;e.addEventListener("mousedown",r=>{H={x:r.clientX,vMin:x,vMax:h},e.style.cursor="grabbing"}),window.addEventListener("mousemove",r=>{if(!H)return;let l=e.getBoundingClientRect(),k=(H.vMax-H.vMin)/(l.width-(D.l+D.r)||1),w=(r.clientX-H.x)*k;V(H.vMin-w,H.vMax-w)}),window.addEventListener("mouseup",()=>{H&&(H=null,e.style.cursor="crosshair")}),e.addEventListener("mousemove",r=>{H||(L=J(r.clientX),j())}),e.addEventListener("mouseleave",()=>{L!==null&&(L=null,s("hover the chart for a point readout"),j())}),e.addEventListener("dblclick",()=>G()),e.style.cursor="crosshair";function he(){f=Z(e),X()}return{repaint:()=>j(),resize:he,setView:V,resetView:G,focusAlert:me}}var ue=!1;function Le(){if(ue)return;ue=!0;let e=`
|
|
2
|
+
.${d}{--term-bg:#211e1a;--term-border:#332f29;--term-text:#c9c2b4;
|
|
3
|
+
--clay:#d15b36;--clay-700:#b4471f;--ink:#1b1916;--paper:#f5f1e8;--surface:#fbf9f3;
|
|
4
|
+
--border:#e6e0d4;--muted:#6e675b;--faint:#9a9384;
|
|
5
|
+
--anom:#d63232;--rec:#36a64f;--nod:#f0ad4e;
|
|
6
|
+
--sans:'Schibsted Grotesk',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
7
|
+
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
8
|
+
font-family:var(--sans);color:var(--ink);}
|
|
9
|
+
.${d} *{box-sizing:border-box;}
|
|
10
|
+
.${d} .dtk-report-root{max-width:1100px;margin:0 auto;padding:20px 18px 40px;}
|
|
11
|
+
/* --- header row ----------------------------------------------------------- */
|
|
12
|
+
.${d} .dtk-header{margin-bottom:16px;padding-left:12px;
|
|
13
|
+
border-left:3px solid var(--clay);}
|
|
14
|
+
.${d} .dtk-h-top{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 14px;}
|
|
15
|
+
.${d} .dtk-title{font-size:21px;font-weight:700;margin:0;color:var(--ink);
|
|
16
|
+
font-family:var(--sans);letter-spacing:-0.01em;}
|
|
17
|
+
.${d} .dtk-meta{font-size:12px;color:var(--muted);font-family:var(--mono);}
|
|
18
|
+
.${d} .dtk-desc{margin:8px 0 0;font-size:13px;color:var(--muted);max-width:760px;
|
|
19
|
+
line-height:1.5;}
|
|
20
|
+
/* --- summary chips (surface cards) ---------------------------------------- */
|
|
21
|
+
.${d} .dtk-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
|
|
22
|
+
.${d} .dtk-chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;
|
|
23
|
+
background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:12px;}
|
|
24
|
+
.${d} .dtk-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
|
|
25
|
+
.${d} .dtk-chip-n{font-weight:700;font-family:var(--mono);color:var(--ink);}
|
|
26
|
+
.${d} .dtk-chip-l{color:var(--faint);font-family:var(--mono);font-size:11px;
|
|
27
|
+
text-transform:uppercase;letter-spacing:0.05em;}
|
|
28
|
+
/* --- detector legend ------------------------------------------------------ */
|
|
29
|
+
.${d} .dtk-legend{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;}
|
|
30
|
+
.${d} .dtk-legend-item{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;
|
|
31
|
+
background:var(--surface);border:1px solid var(--border);border-radius:8px;cursor:pointer;
|
|
32
|
+
color:var(--ink);font-size:12px;font-family:var(--sans);transition:border-color .12s ease;}
|
|
33
|
+
.${d} .dtk-legend-item:hover{border-color:var(--clay);}
|
|
34
|
+
.${d} .dtk-legend-item.off{opacity:0.45;}
|
|
35
|
+
.${d} .dtk-legend-id{color:var(--faint);font-family:var(--mono);font-size:11px;}
|
|
36
|
+
.${d} .dtk-legend-n{color:var(--anom);font-weight:700;font-family:var(--mono);}
|
|
37
|
+
.${d} .dtk-swatch{width:10px;height:10px;border-radius:2px;display:inline-block;}
|
|
38
|
+
/* --- toolbar (presets + readout) ------------------------------------------ */
|
|
39
|
+
.${d} .dtk-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
|
|
40
|
+
gap:8px;margin-bottom:8px;}
|
|
41
|
+
.${d} .dtk-presets{display:flex;gap:5px;}
|
|
42
|
+
.${d} .dtk-preset{padding:5px 13px;background:var(--surface);
|
|
43
|
+
border:1px solid var(--border);border-radius:8px;color:var(--muted);cursor:pointer;
|
|
44
|
+
font-size:12px;font-family:var(--sans);transition:border-color .12s ease,color .12s ease;}
|
|
45
|
+
.${d} .dtk-preset:hover{border-color:var(--clay);color:var(--ink);}
|
|
46
|
+
.${d} .dtk-preset.active{background:var(--clay);color:#fff;border-color:var(--clay);}
|
|
47
|
+
.${d} .dtk-readout{font-size:11px;color:var(--muted);
|
|
48
|
+
font-family:var(--mono);display:flex;flex-wrap:wrap;gap:4px 12px;align-items:center;}
|
|
49
|
+
.${d} .dtk-readout .dtk-swatch{margin-right:4px;}
|
|
50
|
+
.${d} .dtk-ro-t{font-weight:700;color:var(--ink);}
|
|
51
|
+
/* --- chart panel (dark terminal surface) ---------------------------------- */
|
|
52
|
+
.${d} .dtk-chart{position:relative;width:100%;height:360px;background:var(--term-bg);
|
|
53
|
+
border:1px solid var(--term-border);border-radius:12px;overflow:hidden;
|
|
54
|
+
box-shadow:0 24px 60px -30px rgba(27,25,22,.45);}
|
|
55
|
+
.${d} .dtk-chart canvas{width:100%;height:100%;display:block;}
|
|
56
|
+
/* --- alerts list (surface cards) ------------------------------------------ */
|
|
57
|
+
.${d} .dtk-alerts{margin-top:18px;}
|
|
58
|
+
.${d} .dtk-alerts-head{font-size:12px;font-weight:600;color:var(--faint);
|
|
59
|
+
margin-bottom:9px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.06em;}
|
|
60
|
+
.${d} .dtk-alerts-empty{font-size:13px;color:var(--muted);padding:8px 0;}
|
|
61
|
+
.${d} .dtk-alerts-list{display:flex;flex-direction:column;gap:5px;
|
|
62
|
+
max-height:340px;overflow:auto;}
|
|
63
|
+
.${d} .dtk-alert-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
|
|
64
|
+
padding:8px 11px;background:var(--surface);border:1px solid var(--border);
|
|
65
|
+
border-radius:8px;cursor:pointer;color:var(--ink);font-family:var(--sans);
|
|
66
|
+
transition:border-color .12s ease,box-shadow .12s ease;}
|
|
67
|
+
.${d} .dtk-alert-row:hover{border-color:var(--clay);
|
|
68
|
+
box-shadow:0 4px 14px -8px rgba(27,25,22,.35);}
|
|
69
|
+
.${d} .dtk-alert-time{font-size:11px;color:var(--muted);
|
|
70
|
+
font-family:var(--mono);white-space:nowrap;min-width:142px;}
|
|
71
|
+
.${d} .dtk-badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:11px;
|
|
72
|
+
font-weight:700;border:1px solid;text-transform:uppercase;letter-spacing:0.03em;white-space:nowrap;}
|
|
73
|
+
.${d} .dtk-alert-body{display:flex;flex-direction:column;gap:2px;min-width:0;}
|
|
74
|
+
.${d} .dtk-alert-rule{font-size:12px;color:var(--ink);
|
|
75
|
+
font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
76
|
+
.${d} .dtk-alert-sub{font-size:11px;color:var(--muted);}
|
|
77
|
+
`,t=document.createElement("style");t.setAttribute("data-dtk-report",""),t.textContent=e,document.head.appendChild(t)}window.__DTK_REPORT__={render:Me};})();
|