detectkit 0.25.0__tar.gz → 0.26.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 (126) hide show
  1. {detectkit-0.25.0/detectkit.egg-info → detectkit-0.26.1}/PKG-INFO +1 -1
  2. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/__init__.py +1 -1
  3. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/html_labeler.py +74 -17
  4. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/autotune.md +4 -2
  5. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +3 -0
  6. {detectkit-0.25.0 → detectkit-0.26.1/detectkit.egg-info}/PKG-INFO +1 -1
  7. {detectkit-0.25.0 → detectkit-0.26.1}/LICENSE +0 -0
  8. {detectkit-0.25.0 → detectkit-0.26.1}/MANIFEST.in +0 -0
  9. {detectkit-0.25.0 → detectkit-0.26.1}/README.md +0 -0
  10. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/__init__.py +0 -0
  11. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/__init__.py +0 -0
  12. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/base.py +0 -0
  13. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/branding.py +0 -0
  14. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/email.py +0 -0
  15. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/factory.py +0 -0
  16. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/mattermost.py +0 -0
  17. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/slack.py +0 -0
  18. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/telegram.py +0 -0
  19. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/channels/webhook.py +0 -0
  20. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
  21. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_base.py +0 -0
  22. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  23. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
  24. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  25. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  26. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/_types.py +0 -0
  27. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  28. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/__init__.py +0 -0
  29. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/_base.py +0 -0
  30. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/_types.py +0 -0
  31. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/autotuner.py +0 -0
  32. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/config_emitter.py +0 -0
  33. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/crossval.py +0 -0
  34. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/detector_select.py +0 -0
  35. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/distribution.py +0 -0
  36. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/grid_search.py +0 -0
  37. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/label_server.py +0 -0
  38. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/labels.py +0 -0
  39. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/result.py +0 -0
  40. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/scoring.py +0 -0
  41. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/seasonality_search.py +0 -0
  42. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/settings.py +0 -0
  43. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/autotune/window_select.py +0 -0
  44. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/__init__.py +0 -0
  45. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/_output.py +0 -0
  46. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  47. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  48. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  49. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  50. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  51. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  52. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/rules/project.md +0 -0
  53. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  54. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  55. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  56. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/__init__.py +0 -0
  57. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/autotune.py +0 -0
  58. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/clean.py +0 -0
  59. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/init.py +0 -0
  60. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/init_claude.py +0 -0
  61. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/run.py +0 -0
  62. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/test_alert.py +0 -0
  63. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/commands/unlock.py +0 -0
  64. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/cli/main.py +0 -0
  65. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/config/__init__.py +0 -0
  66. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/config/metric_config.py +0 -0
  67. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/config/profile.py +0 -0
  68. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/config/project_config.py +0 -0
  69. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/config/validator.py +0 -0
  70. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/core/__init__.py +0 -0
  71. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/core/interval.py +0 -0
  72. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/core/models.py +0 -0
  73. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/__init__.py +0 -0
  74. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/_sql_manager.py +0 -0
  75. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/clickhouse_manager.py +0 -0
  76. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/__init__.py +0 -0
  77. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
  78. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  79. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_base.py +0 -0
  80. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
  81. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_detections.py +0 -0
  82. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
  83. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_metrics.py +0 -0
  84. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_schema.py +0 -0
  85. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/_tasks.py +0 -0
  86. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/internal_tables/manager.py +0 -0
  87. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/manager.py +0 -0
  88. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/mysql_manager.py +0 -0
  89. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/postgres_manager.py +0 -0
  90. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/database/tables.py +0 -0
  91. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/__init__.py +0 -0
  92. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/base.py +0 -0
  93. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/factory.py +0 -0
  94. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/seasonality.py +0 -0
  95. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/__init__.py +0 -0
  96. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/_windowed.py +0 -0
  97. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/iqr.py +0 -0
  98. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/mad.py +0 -0
  99. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  100. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/detectors/statistical/zscore.py +0 -0
  101. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/loaders/__init__.py +0 -0
  102. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/loaders/metric_loader.py +0 -0
  103. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/loaders/query_template.py +0 -0
  104. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/__init__.py +0 -0
  105. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/error_dispatch.py +0 -0
  106. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
  107. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  108. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/_base.py +0 -0
  109. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  110. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  111. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/_types.py +0 -0
  112. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/orchestration/task_manager/manager.py +0 -0
  113. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/utils/__init__.py +0 -0
  114. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/utils/datetime_utils.py +0 -0
  115. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/utils/env_interpolation.py +0 -0
  116. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/utils/json_utils.py +0 -0
  117. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit/utils/stats.py +0 -0
  118. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit.egg-info/SOURCES.txt +0 -0
  119. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit.egg-info/dependency_links.txt +0 -0
  120. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit.egg-info/entry_points.txt +0 -0
  121. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit.egg-info/requires.txt +0 -0
  122. {detectkit-0.25.0 → detectkit-0.26.1}/detectkit.egg-info/top_level.txt +0 -0
  123. {detectkit-0.25.0 → detectkit-0.26.1}/pyproject.toml +0 -0
  124. {detectkit-0.25.0 → detectkit-0.26.1}/requirements.txt +0 -0
  125. {detectkit-0.25.0 → detectkit-0.26.1}/setup.cfg +0 -0
  126. {detectkit-0.25.0 → detectkit-0.26.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.25.0
3
+ Version: 0.26.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.25.0"
7
+ __version__ = "0.26.1"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -121,6 +121,9 @@ _TEMPLATE = """<!doctype html>
121
121
  .thbar input.num { width: 84px; font-family: var(--mono); }
122
122
  .thbar input:focus, .thbar select:focus { outline: none; border-color: var(--nodata); }
123
123
  .thbar button { padding: 7px 13px; }
124
+ .thbar .thscope { color: var(--faint); font-size: 12px; white-space: nowrap; }
125
+ .thbar .thscope.hint { font-style: italic; }
126
+ .thbar .thscope b { color: var(--nodata); font-weight: 600; font-style: normal; }
124
127
  canvas#c { width: 100%; height: clamp(300px, 44vh, 500px); display:block; touch-action: none;
125
128
  background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
126
129
  .zoombar { display:flex; align-items:center; gap:8px; margin: 10px 0 6px; }
@@ -177,6 +180,8 @@ _TEMPLATE = """<!doctype html>
177
180
  <label>bridge gaps ≤
178
181
  <input id="thgap" class="num" type="number" min="0" step="1" value="0" /> intervals
179
182
  </label>
183
+ <span id="thscope" class="thscope"></span>
184
+ <button id="thwin" class="ghost" style="display:none" title="capture across the whole current view again">↺ whole view</button>
180
185
  <button id="thadd" class="primary" disabled>Add 0 spans</button>
181
186
  <button id="thdone" class="ghost">Done</button>
182
187
  </div>
@@ -189,8 +194,10 @@ _TEMPLATE = """<!doctype html>
189
194
  <canvas id="ov" aria-label="navigator — drag the window to pan, its edges to stretch the view"></canvas>
190
195
  <div class="navhint">Drag on an empty area to mark an incident · drag an existing incident's edges to
191
196
  adjust it, or its middle to move it · click its <b>✕</b> (or select it and press Delete) to remove it ·
192
- use <b>Threshold capture</b> to grab every span past a horizontal line at once · <b>focus</b> a row to jump
193
- the chart to it · scroll to zoom, double-click to reset · drag the navigator window below to pan.</div>
197
+ use <b>Threshold capture</b> to grab every span past a horizontal line at once (click sets the line; drag
198
+ across the chart to limit it to a time window handy when the metric behaves differently across periods) ·
199
+ <b>focus</b> a row to jump the chart to it · scroll to zoom, double-click to reset · drag the navigator
200
+ window below to pan.</div>
194
201
  <div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
195
202
  <ul id="list"></ul>
196
203
  <footer>All times UTC · self-contained, nothing leaves your browser · re-label any time —
@@ -232,6 +239,9 @@ let dpr = 1, hover = null, dragging = null, ovAct = null;
232
239
  // selObj: the currently selected incident (object ref, survives re-sorting);
233
240
  // hoverRow/hoverDel: list-row / ✕-handle hover targets; threshold-capture state.
234
241
  let selObj = null, hoverRow = -1, hoverDel = -1, thMode = false, thHover = null;
242
+ // Threshold-capture window: thDown tracks a press, thDragWin a live drag, capWin
243
+ // the committed custom window (null → capture within the current view).
244
+ let thDown = null, thDragWin = null, capWin = null;
235
245
 
236
246
  const clamp = (x,a,b) => Math.max(a, Math.min(b, x));
237
247
  const vspan = () => viewMax - viewMin;
@@ -304,18 +314,27 @@ const vAt = clientY => { const r=c.getBoundingClientRect();
304
314
  // The active threshold value: the locked input wins, else the live cursor value.
305
315
  function thEff() { const s=thvalEl.value.trim();
306
316
  return (s!=='' && !isNaN(Number(s))) ? Number(s) : thHover; }
307
- // Contiguous runs of points on the chosen side of the line, bridging short gaps.
317
+ // The active capture window: a live/committed custom window, else the current
318
+ // view — so the threshold only grabs the period you're looking at (or painted),
319
+ // not the whole series. Returns [lo, hi] in ms.
320
+ function capRange() {
321
+ const w = thDragWin || capWin;
322
+ if (w) return [Math.min(w.a,w.b), Math.max(w.a,w.b)];
323
+ return [viewMin, viewMax]; }
324
+ // Contiguous runs of points on the chosen side of the line within the capture
325
+ // window, bridging short gaps.
308
326
  function thRuns() { const val=thEff(); if (val===null) return [];
309
327
  const dir=thdirEl.value, gapMax=Math.max(0, parseInt(thgapEl.value)||0);
328
+ const win=capRange(), lo=win[0], hi=win[1];
310
329
  const runs=[]; let s=null, e=null, gap=0;
311
- for (let i=0;i<N;i++) { const p=pts[i];
330
+ for (let i=0;i<N;i++) { const p=pts[i]; if (p.ts<lo || p.ts>hi) continue;
312
331
  const q = p.v!==null && (dir==='above' ? p.v>val : p.v<val);
313
332
  if (q) { if (s===null) s=p.ts; e=p.ts; gap=0; }
314
333
  else if (s!==null) { gap++; if (gap>gapMax) { runs.push([s,e]); s=null; gap=0; } } }
315
334
  if (s!==null) runs.push([s,e]);
316
335
  return runs; }
317
336
  function thCount() { const n=thRuns().length;
318
- thaddEl.textContent = 'Add '+n+' span'+(n===1?'':'s'); thaddEl.disabled = n===0; }
337
+ thaddEl.textContent = 'Add '+n+' span'+(n===1?'':'s'); thaddEl.disabled = n===0; updateThWin(); }
319
338
  // Add a captured span, merging it into any overlapping incidents (a single span
320
339
  // can bridge several) into one band that keeps the first one's label.
321
340
  function addCaptured(a,b) {
@@ -328,8 +347,8 @@ function addCaptured(a,b) {
328
347
  if (host===null) incidents.push({a, b, label:''}); }
329
348
  function toggleTh(on) { thMode = (on===undefined) ? !thMode : !!on;
330
349
  thbtnEl.classList.toggle('active', thMode); thbarEl.style.display = thMode ? 'flex' : 'none';
331
- if (thMode) { hover=null; } else { thHover=null; }
332
- c.style.cursor = thMode ? 'row-resize' : 'crosshair'; if (thMode) thCount(); draw(); }
350
+ if (thMode) { hover=null; } else { thHover=null; thDown=null; thDragWin=null; capWin=null; updateThWin(); }
351
+ c.style.cursor = thMode ? 'crosshair' : 'crosshair'; if (thMode) { updateThWin(); thCount(); } draw(); }
333
352
 
334
353
  function rr(g,x,y,w,h,r) { g.beginPath();
335
354
  g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r);
@@ -376,9 +395,20 @@ function draw() {
376
395
  if (dragging && dragging.mode==='new') { const x0=px(dragging.a), x1=px(dragging.b);
377
396
  ctx.fillStyle='rgba(240,173,78,0.28)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); }
378
397
  drawSeries(ctx, px, py, viewMin, viewMax, M.l*dpr, plotW(), '#d15b36', 1.5);
379
- if (thMode) { const val=thEff(); if (val!==null && val>=vmin && val<=vmax) { const yy=py(val);
380
- ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1.5*dpr; ctx.setLineDash([6*dpr,4*dpr]);
381
- ctx.beginPath(); ctx.moveTo(M.l*dpr, yy); ctx.lineTo(c.width-M.r*dpr, yy); ctx.stroke(); ctx.setLineDash([]); } }
398
+ if (thMode) {
399
+ const win=capRange(), narrow = !!(thDragWin || capWin);
400
+ const xlo=clamp(px(win[0]), M.l*dpr, c.width-M.r*dpr), xhi=clamp(px(win[1]), M.l*dpr, c.width-M.r*dpr);
401
+ if (narrow) { // dim everything outside the capture window
402
+ ctx.fillStyle='rgba(27,25,22,0.5)';
403
+ ctx.fillRect(M.l*dpr, M.t*dpr, xlo-M.l*dpr, plotH());
404
+ ctx.fillRect(xhi, M.t*dpr, (c.width-M.r*dpr)-xhi, plotH());
405
+ ctx.strokeStyle='rgba(240,173,78,0.7)'; ctx.lineWidth=1*dpr; ctx.setLineDash([3*dpr,3*dpr]);
406
+ ctx.beginPath(); ctx.moveTo(xlo,M.t*dpr); ctx.lineTo(xlo,c.height-M.b*dpr);
407
+ ctx.moveTo(xhi,M.t*dpr); ctx.lineTo(xhi,c.height-M.b*dpr); ctx.stroke(); ctx.setLineDash([]); }
408
+ const val=thEff();
409
+ if (val!==null && val>=vmin && val<=vmax) { const yy=py(val);
410
+ ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1.5*dpr; ctx.setLineDash([6*dpr,4*dpr]);
411
+ ctx.beginPath(); ctx.moveTo(xlo, yy); ctx.lineTo(xhi, yy); ctx.stroke(); ctx.setLineDash([]); } }
382
412
  ctx.restore();
383
413
  if (thMode) drawThLabel();
384
414
  else if (dragging && !ovAct) drawDragLabel();
@@ -388,9 +418,13 @@ function draw() {
388
418
  // Readout while picking a threshold: the line value, side, and how many spans
389
419
  // would be captured.
390
420
  function drawThLabel() {
391
- const val=thEff(); if (val===null) return;
392
- const n=thRuns().length;
393
- const text='line '+fmtVal(val)+' · '+thdirEl.value+' · '+n+' span'+(n===1?'':'s');
421
+ const val=thEff(), win=capRange(), narrow = !!(thDragWin || capWin);
422
+ let text;
423
+ if (val===null) { // no line yet — prompt how to use the mode
424
+ text = 'drag the chart to pick a period · hover or type a value to set the line';
425
+ } else { const n=thRuns().length;
426
+ text='line '+fmtVal(val)+' · '+thdirEl.value+' · '+n+' span'+(n===1?'':'s')
427
+ + (narrow ? (' · '+fmtDur(win[1]-win[0])+' window') : ''); }
394
428
  ctx.font=(11*dpr)+'px ui-monospace, monospace';
395
429
  const tw=ctx.measureText(text).width, bw=tw+14*dpr, bh=22*dpr, bx=M.l*dpr+6*dpr, by=M.t*dpr+2;
396
430
  ctx.fillStyle='rgba(27,25,22,0.96)'; ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1*dpr;
@@ -491,7 +525,9 @@ function hitIncident(clientX, clientY) {
491
525
  return null;
492
526
  }
493
527
  c.addEventListener('mousedown', e => {
494
- if (thMode) { thvalEl.value = String(Math.round(vAt(e.clientY)*1000)/1000); thCount(); draw(); return; }
528
+ // In threshold mode a press either sets the line (a click) or paints a capture
529
+ // window (a horizontal drag) — resolved on mouseup by how far it moved.
530
+ if (thMode) { thDown = {x:e.clientX, ts:tsAt(e.clientX)}; thHover = vAt(e.clientY); thCount(); draw(); return; }
495
531
  const hit = hitIncident(e.clientX, e.clientY), t = tsAt(e.clientX);
496
532
  if (hit && hit.edge==='del') { removeIncident(incidents[hit.i]); return; }
497
533
  if (hit && hit.edge==='move') { const iv=incidents[hit.i]; selObj=iv;
@@ -501,7 +537,10 @@ c.addEventListener('mousedown', e => {
501
537
  else { selObj=null; dragging={mode:'new', a:t, b:t, sx:e.clientX, cx:e.clientX}; draw(); }
502
538
  });
503
539
  c.addEventListener('mousemove', e => { if (ovAct) return;
504
- if (thMode && !dragging) { thHover=vAt(e.clientY); thCount(); draw(); return; }
540
+ if (thMode && !dragging) {
541
+ if (thDown && Math.abs(e.clientX - thDown.x) > 6) { thDragWin = {a: thDown.ts, b: tsAt(e.clientX)}; }
542
+ else { if (thDown) thDragWin = null; thHover = vAt(e.clientY); }
543
+ thCount(); draw(); return; }
505
544
  if (dragging) {
506
545
  dragging.cx=e.clientX; const t=tsAt(e.clientX);
507
546
  if (dragging.mode==='new') { dragging.b=t; }
@@ -539,8 +578,16 @@ window.addEventListener('mousemove', e => { if (!ovAct) return; const t=ovTsAtCs
539
578
  if (ovAct.type==='l') setView(Math.min(t, viewMax-minSpan), viewMax);
540
579
  else if (ovAct.type==='r') setView(viewMin, Math.max(t, viewMin+minSpan));
541
580
  else { const d=t-ovAct.grab; setView(ovAct.vMin+d, ovAct.vMax+d); } });
542
- window.addEventListener('mouseup', () => {
581
+ window.addEventListener('mouseup', e => {
543
582
  if (ovAct) { ovAct=null; return; }
583
+ if (thMode && thDown) {
584
+ if (Math.abs(e.clientX - thDown.x) > 6) { // a drag → set the capture window
585
+ const a=thDown.ts, b=tsAt(e.clientX); capWin = {a:Math.min(a,b), b:Math.max(a,b)};
586
+ } else { // a click → set the threshold line value
587
+ thvalEl.value = String(Math.round(vAt(e.clientY)*1000)/1000);
588
+ }
589
+ thDown=null; thDragWin=null; updateThWin(); thCount(); draw(); return;
590
+ }
544
591
  if (!dragging) return;
545
592
  if (dragging.mode==='new') {
546
593
  if (Math.abs(dragging.cx-dragging.sx) > 4) {
@@ -574,7 +621,17 @@ window.focusInc = i => { const iv=incidents[i]; if (!iv) return; selObj=iv;
574
621
  const thbtnEl=document.getElementById('thbtn'), thbarEl=document.getElementById('thbar');
575
622
  const thvalEl=document.getElementById('thval'), thdirEl=document.getElementById('thdir');
576
623
  const thgapEl=document.getElementById('thgap'), thaddEl=document.getElementById('thadd');
577
- const thdoneEl=document.getElementById('thdone');
624
+ const thdoneEl=document.getElementById('thdone'), thwinEl=document.getElementById('thwin');
625
+ const thscopeEl=document.getElementById('thscope');
626
+ // Always-visible scope readout so the time-window control is discoverable
627
+ // (the ✕/↺ reset only appears once a window exists).
628
+ function updateThWin() {
629
+ thwinEl.style.display = capWin ? '' : 'none';
630
+ if (!thscopeEl) return;
631
+ const w = thDragWin || capWin;
632
+ if (w) { const r=capRange(); thscopeEl.innerHTML = 'period: <b>'+fmtDur(r[1]-r[0])+'</b>'; thscopeEl.className='thscope'; }
633
+ else { thscopeEl.textContent = 'period: current view — drag the chart to limit it'; thscopeEl.className='thscope hint'; } }
634
+ thwinEl.onclick = () => { capWin=null; updateThWin(); thCount(); draw(); };
578
635
  thbtnEl.onclick = () => toggleTh();
579
636
  thdoneEl.onclick = () => toggleTh(false);
580
637
  thdirEl.onchange = () => { thCount(); draw(); };
@@ -99,8 +99,10 @@ user to recall timestamps** — it is the easiest, most reliable path:
99
99
  move, click-drag to mark, drag an incident's edges to adjust, add a
100
100
  description, optionally name the set), then clicks **Save & tune**. For many
101
101
  clear outliers, **Threshold capture** grabs every span past a horizontal line
102
- at once (above/below, with an optional gap-bridge); each band's (or
103
- select + Delete) removes one, and **focus** on a list row jumps the chart to it.
102
+ at once (above/below, with an optional gap-bridge); it captures within the
103
+ current view by default, and dragging across the chart limits it to a time
104
+ window (different boundary per period). Each band's ✕ (or select + Delete)
105
+ removes one, and **focus** on a list row jumps the chart to it.
104
106
  3. That writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml` automatically
105
107
  (named after the metric, optional set name as a suffix; versioned —
106
108
  re-labeling never overwrites) and the **same command continues into the tuning
@@ -101,6 +101,9 @@ user through it:
101
101
  3. When many outliers are obvious, use **Threshold capture**: set a horizontal
102
102
  line (hover the chart or type a value), pick **above/below**, optionally bridge
103
103
  small gaps, and **Add N spans** marks them all at once — then tidy with the ✕.
104
+ It captures within the current view by default; **drag across the chart** to
105
+ limit it to a time window (so a metric that behaves differently across periods
106
+ can take a different boundary in each). **↺ whole view** clears the window.
104
107
  4. Click **Save & tune**. The server writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml`
105
108
  automatically (named after the metric, with the optional set name as a suffix;
106
109
  versioned — re-labeling never overwrites) and the **same command
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.25.0
3
+ Version: 0.26.1
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes