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.
Files changed (139) hide show
  1. {detectkit-0.29.0 → detectkit-0.30.1}/MANIFEST.in +1 -0
  2. {detectkit-0.29.0/detectkit.egg-info → detectkit-0.30.1}/PKG-INFO +1 -1
  3. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/__init__.py +1 -1
  4. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/html_labeler.py +67 -47
  5. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/autotune.md +21 -2
  6. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/cli.md +21 -0
  7. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/overview.md +15 -0
  8. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +6 -0
  9. detectkit-0.30.1/detectkit/cli/commands/tune.py +108 -0
  10. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/main.py +75 -0
  11. detectkit-0.30.1/detectkit/reporting/assets/report.js +77 -0
  12. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/builder.py +125 -1
  13. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/html_report.py +4 -0
  14. detectkit-0.30.1/detectkit/tuning/__init__.py +34 -0
  15. detectkit-0.30.1/detectkit/tuning/assets/tune.js +51 -0
  16. detectkit-0.30.1/detectkit/tuning/config_writer.py +137 -0
  17. detectkit-0.30.1/detectkit/tuning/html.py +82 -0
  18. detectkit-0.30.1/detectkit/tuning/payload.py +194 -0
  19. detectkit-0.30.1/detectkit/tuning/server.py +179 -0
  20. {detectkit-0.29.0 → detectkit-0.30.1/detectkit.egg-info}/PKG-INFO +1 -1
  21. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/SOURCES.txt +7 -0
  22. {detectkit-0.29.0 → detectkit-0.30.1}/pyproject.toml +3 -0
  23. detectkit-0.29.0/detectkit/reporting/assets/report.js +0 -62
  24. {detectkit-0.29.0 → detectkit-0.30.1}/LICENSE +0 -0
  25. {detectkit-0.29.0 → detectkit-0.30.1}/README.md +0 -0
  26. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/__init__.py +0 -0
  27. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/__init__.py +0 -0
  28. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/base.py +0 -0
  29. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/branding.py +0 -0
  30. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/email.py +0 -0
  31. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/factory.py +0 -0
  32. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/mattermost.py +0 -0
  33. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/slack.py +0 -0
  34. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/telegram.py +0 -0
  35. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/channels/webhook.py +0 -0
  36. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
  37. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_base.py +0 -0
  38. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  39. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
  40. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  41. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  42. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_replay.py +0 -0
  43. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/_types.py +0 -0
  44. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  45. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/__init__.py +0 -0
  46. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/_base.py +0 -0
  47. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/_types.py +0 -0
  48. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/autotuner.py +0 -0
  49. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/config_emitter.py +0 -0
  50. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/crossval.py +0 -0
  51. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/detector_select.py +0 -0
  52. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/distribution.py +0 -0
  53. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/grid_search.py +0 -0
  54. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/label_server.py +0 -0
  55. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/labels.py +0 -0
  56. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/result.py +0 -0
  57. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/scoring.py +0 -0
  58. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/seasonality_search.py +0 -0
  59. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/settings.py +0 -0
  60. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/autotune/window_select.py +0 -0
  61. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/__init__.py +0 -0
  62. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/_output.py +0 -0
  63. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  64. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  65. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  66. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  67. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/rules/project.md +0 -0
  68. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  69. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  70. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  71. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/__init__.py +0 -0
  72. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/autotune.py +0 -0
  73. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/clean.py +0 -0
  74. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/init.py +0 -0
  75. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/init_claude.py +0 -0
  76. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/run.py +0 -0
  77. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/test_alert.py +0 -0
  78. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/cli/commands/unlock.py +0 -0
  79. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/__init__.py +0 -0
  80. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/metric_config.py +0 -0
  81. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/profile.py +0 -0
  82. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/project_config.py +0 -0
  83. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/config/validator.py +0 -0
  84. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/__init__.py +0 -0
  85. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/interval.py +0 -0
  86. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/core/models.py +0 -0
  87. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/__init__.py +0 -0
  88. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/_sql_manager.py +0 -0
  89. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/clickhouse_manager.py +0 -0
  90. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/__init__.py +0 -0
  91. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
  92. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  93. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_base.py +0 -0
  94. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
  95. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_detections.py +0 -0
  96. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
  97. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_metrics.py +0 -0
  98. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_schema.py +0 -0
  99. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/_tasks.py +0 -0
  100. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/internal_tables/manager.py +0 -0
  101. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/manager.py +0 -0
  102. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/mysql_manager.py +0 -0
  103. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/postgres_manager.py +0 -0
  104. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/database/tables.py +0 -0
  105. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/__init__.py +0 -0
  106. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/base.py +0 -0
  107. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/factory.py +0 -0
  108. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/seasonality.py +0 -0
  109. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/__init__.py +0 -0
  110. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/_windowed.py +0 -0
  111. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/iqr.py +0 -0
  112. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/mad.py +0 -0
  113. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  114. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/detectors/statistical/zscore.py +0 -0
  115. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/__init__.py +0 -0
  116. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/metric_loader.py +0 -0
  117. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/loaders/query_template.py +0 -0
  118. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/__init__.py +0 -0
  119. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/error_dispatch.py +0 -0
  120. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
  121. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  122. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_base.py +0 -0
  123. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  124. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  125. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/_types.py +0 -0
  126. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/orchestration/task_manager/manager.py +0 -0
  127. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/reporting/__init__.py +0 -0
  128. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/__init__.py +0 -0
  129. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/datetime_utils.py +0 -0
  130. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/env_interpolation.py +0 -0
  131. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/json_utils.py +0 -0
  132. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit/utils/stats.py +0 -0
  133. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/dependency_links.txt +0 -0
  134. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/entry_points.txt +0 -0
  135. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/requires.txt +0 -0
  136. {detectkit-0.29.0 → detectkit-0.30.1}/detectkit.egg-info/top_level.txt +0 -0
  137. {detectkit-0.29.0 → detectkit-0.30.1}/requirements.txt +0 -0
  138. {detectkit-0.29.0 → detectkit-0.30.1}/setup.cfg +0 -0
  139. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.29.0
3
+ Version: 0.30.1
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -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.29.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; --paper:#f5f1e8; --muted:#6e675b; --faint:#9a9384;
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(--term-bg); color: var(--term-text);
84
+ body { font-family: var(--ui); margin: 0; background: var(--paper); color: var(--ink);
80
85
  -webkit-font-smoothing: antialiased; }
81
- .shell { max-width: 1080px; margin: 0 auto; padding: 22px 22px 44px; }
82
- .brand { display:flex; align-items:center; gap:9px; margin-bottom: 14px; }
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(--paper); font-weight: 600; font-size: 15px; letter-spacing: .2px; }
85
- .brand span { color: var(--faint); font-size: 12px; }
86
- h1 { font-size: 18px; line-height: 1.3; margin: 0 0 6px; color: var(--paper); font-weight: 600; }
87
- h1 code { color: var(--clay); font-family: var(--mono); font-size: .82em; }
88
- .ichip { display:inline-flex; align-items:center; gap:6px; vertical-align: middle; margin-left: 8px;
89
- font-family: var(--mono); font-size: 12px; font-weight: 500; color: var(--paper);
90
- background: rgba(209,91,54,0.16); border: 1px solid var(--clay); border-radius: 999px; padding: 3px 10px; }
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(--faint); font-size: 13px; margin: 0 0 18px; line-height: 1.55; }
94
- .hint code, code.k { color: var(--term-text); font-family: var(--mono); font-size: 12px;
95
- background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 5px; padding: 1px 6px; }
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(--term-border); color: var(--faint); cursor: default; }
102
- button.ghost { background: transparent; color: var(--term-text); border: 1px solid var(--term-border); }
103
- button.ghost:hover { border-color: var(--faint); color: var(--paper); }
104
- button.ghost.active { border-color: var(--nodata); color: var(--paper); background: rgba(240,173,78,0.16); }
105
- input.setname { background: var(--term-surface); color: var(--paper); border: 1px solid var(--term-border);
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(--muted); }
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(--faint); font-size: 12.5px; font-family: var(--mono); }
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, #2e9e73); }
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(--faint); }
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(--term-surface); }
117
- .thbar .thlabel { color: var(--nodata); font-size: 12.5px; font-weight: 600; }
118
- .thbar label { color: var(--faint); font-size: 12.5px; display:inline-flex; align-items:center; gap:6px; }
119
- .thbar select, .thbar input { background: var(--term-bg); color: var(--paper); border: 1px solid var(--term-border);
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(--nodata); font-weight: 600; font-style: normal; }
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-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
129
- .zoombar { display:flex; align-items:center; gap:8px; margin: 10px 0 6px; }
130
- .rangelbl { margin-left: auto; color: var(--faint); font-size: 12px; font-family: var(--mono); }
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-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: grab; }
133
- .navhint { color: var(--faint); font-size: 12px; margin: 7px 2px 0; line-height: 1.55; }
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(--term-border); border-radius: 8px; margin-bottom: 7px; background: var(--term-surface); }
138
- li.sel { border-color: var(--clay); background: rgba(209,91,54,0.10); }
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(--term-text); }
141
- li .dur { color: var(--faint); font-size: 12px; }
142
- li input.desc { flex: 1 1 220px; min-width: 160px; background: var(--term-bg); color: var(--paper);
143
- border: 1px solid var(--term-border); border-radius: 6px; padding: 6px 9px; font-family: var(--ui); font-size: 12.5px; }
144
- li input.desc::placeholder { color: var(--muted); }
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(--term-border);
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(--term-text); }
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
- <h1>Label incidents — <code>__METRIC__</code><span id="intervalchip" class="ichip"
159
- title="The metric's sampling interval — the spacing between points, taken straight from the metric."></span></h1>
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
- To see the winning detector at work, join recent datapoints with its detections
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.
@@ -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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),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};})();