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