detectkit 0.25.0__tar.gz → 0.26.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {detectkit-0.25.0/detectkit.egg-info → detectkit-0.26.0}/PKG-INFO +1 -1
  2. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/html_labeler.py +57 -15
  4. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/autotune.md +4 -2
  5. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +3 -0
  6. {detectkit-0.25.0 → detectkit-0.26.0/detectkit.egg-info}/PKG-INFO +1 -1
  7. {detectkit-0.25.0 → detectkit-0.26.0}/LICENSE +0 -0
  8. {detectkit-0.25.0 → detectkit-0.26.0}/MANIFEST.in +0 -0
  9. {detectkit-0.25.0 → detectkit-0.26.0}/README.md +0 -0
  10. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/__init__.py +0 -0
  11. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/__init__.py +0 -0
  12. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/base.py +0 -0
  13. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/branding.py +0 -0
  14. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/email.py +0 -0
  15. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/factory.py +0 -0
  16. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/mattermost.py +0 -0
  17. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/slack.py +0 -0
  18. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/telegram.py +0 -0
  19. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/channels/webhook.py +0 -0
  20. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  21. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  22. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  23. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  24. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  25. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  26. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  27. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  28. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/__init__.py +0 -0
  29. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/_base.py +0 -0
  30. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/_types.py +0 -0
  31. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/autotuner.py +0 -0
  32. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/config_emitter.py +0 -0
  33. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/crossval.py +0 -0
  34. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/detector_select.py +0 -0
  35. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/distribution.py +0 -0
  36. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/grid_search.py +0 -0
  37. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/label_server.py +0 -0
  38. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/labels.py +0 -0
  39. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/result.py +0 -0
  40. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/scoring.py +0 -0
  41. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/seasonality_search.py +0 -0
  42. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/settings.py +0 -0
  43. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/autotune/window_select.py +0 -0
  44. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/__init__.py +0 -0
  45. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/_output.py +0 -0
  46. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  47. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  48. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  49. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  50. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  51. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  52. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  53. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  54. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  55. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  56. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/__init__.py +0 -0
  57. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/autotune.py +0 -0
  58. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/clean.py +0 -0
  59. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/init.py +0 -0
  60. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/init_claude.py +0 -0
  61. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/run.py +0 -0
  62. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/test_alert.py +0 -0
  63. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/commands/unlock.py +0 -0
  64. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/cli/main.py +0 -0
  65. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/config/__init__.py +0 -0
  66. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/config/metric_config.py +0 -0
  67. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/config/profile.py +0 -0
  68. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/config/project_config.py +0 -0
  69. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/config/validator.py +0 -0
  70. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/core/__init__.py +0 -0
  71. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/core/interval.py +0 -0
  72. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/core/models.py +0 -0
  73. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/__init__.py +0 -0
  74. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/_sql_manager.py +0 -0
  75. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/clickhouse_manager.py +0 -0
  76. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/__init__.py +0 -0
  77. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  78. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  79. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_base.py +0 -0
  80. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  81. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_detections.py +0 -0
  82. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  83. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  84. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_schema.py +0 -0
  85. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  86. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/internal_tables/manager.py +0 -0
  87. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/manager.py +0 -0
  88. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/mysql_manager.py +0 -0
  89. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/postgres_manager.py +0 -0
  90. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/database/tables.py +0 -0
  91. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/__init__.py +0 -0
  92. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/base.py +0 -0
  93. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/factory.py +0 -0
  94. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/seasonality.py +0 -0
  95. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/__init__.py +0 -0
  96. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  97. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/iqr.py +0 -0
  98. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/mad.py +0 -0
  99. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  100. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/detectors/statistical/zscore.py +0 -0
  101. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/loaders/__init__.py +0 -0
  102. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/loaders/metric_loader.py +0 -0
  103. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/loaders/query_template.py +0 -0
  104. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/__init__.py +0 -0
  105. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/error_dispatch.py +0 -0
  106. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  107. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  108. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  109. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  110. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  111. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  112. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  113. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/utils/__init__.py +0 -0
  114. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/utils/datetime_utils.py +0 -0
  115. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/utils/env_interpolation.py +0 -0
  116. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/utils/json_utils.py +0 -0
  117. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit/utils/stats.py +0 -0
  118. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit.egg-info/SOURCES.txt +0 -0
  119. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit.egg-info/dependency_links.txt +0 -0
  120. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit.egg-info/entry_points.txt +0 -0
  121. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit.egg-info/requires.txt +0 -0
  122. {detectkit-0.25.0 → detectkit-0.26.0}/detectkit.egg-info/top_level.txt +0 -0
  123. {detectkit-0.25.0 → detectkit-0.26.0}/pyproject.toml +0 -0
  124. {detectkit-0.25.0 → detectkit-0.26.0}/requirements.txt +0 -0
  125. {detectkit-0.25.0 → detectkit-0.26.0}/setup.cfg +0 -0
  126. {detectkit-0.25.0 → detectkit-0.26.0}/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.0
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.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -177,6 +177,7 @@ _TEMPLATE = """<!doctype html>
177
177
  <label>bridge gaps ≤
178
178
  <input id="thgap" class="num" type="number" min="0" step="1" value="0" /> intervals
179
179
  </label>
180
+ <button id="thwin" class="ghost" style="display:none" title="capture across the whole current view again">↺ whole view</button>
180
181
  <button id="thadd" class="primary" disabled>Add 0 spans</button>
181
182
  <button id="thdone" class="ghost">Done</button>
182
183
  </div>
@@ -189,8 +190,10 @@ _TEMPLATE = """<!doctype html>
189
190
  <canvas id="ov" aria-label="navigator — drag the window to pan, its edges to stretch the view"></canvas>
190
191
  <div class="navhint">Drag on an empty area to mark an incident · drag an existing incident's edges to
191
192
  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>
193
+ use <b>Threshold capture</b> to grab every span past a horizontal line at once (click sets the line; drag
194
+ across the chart to limit it to a time window handy when the metric behaves differently across periods) ·
195
+ <b>focus</b> a row to jump the chart to it · scroll to zoom, double-click to reset · drag the navigator
196
+ window below to pan.</div>
194
197
  <div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
195
198
  <ul id="list"></ul>
196
199
  <footer>All times UTC · self-contained, nothing leaves your browser · re-label any time —
@@ -232,6 +235,9 @@ let dpr = 1, hover = null, dragging = null, ovAct = null;
232
235
  // selObj: the currently selected incident (object ref, survives re-sorting);
233
236
  // hoverRow/hoverDel: list-row / ✕-handle hover targets; threshold-capture state.
234
237
  let selObj = null, hoverRow = -1, hoverDel = -1, thMode = false, thHover = null;
238
+ // Threshold-capture window: thDown tracks a press, thDragWin a live drag, capWin
239
+ // the committed custom window (null → capture within the current view).
240
+ let thDown = null, thDragWin = null, capWin = null;
235
241
 
236
242
  const clamp = (x,a,b) => Math.max(a, Math.min(b, x));
237
243
  const vspan = () => viewMax - viewMin;
@@ -304,11 +310,20 @@ const vAt = clientY => { const r=c.getBoundingClientRect();
304
310
  // The active threshold value: the locked input wins, else the live cursor value.
305
311
  function thEff() { const s=thvalEl.value.trim();
306
312
  return (s!=='' && !isNaN(Number(s))) ? Number(s) : thHover; }
307
- // Contiguous runs of points on the chosen side of the line, bridging short gaps.
313
+ // The active capture window: a live/committed custom window, else the current
314
+ // view — so the threshold only grabs the period you're looking at (or painted),
315
+ // not the whole series. Returns [lo, hi] in ms.
316
+ function capRange() {
317
+ const w = thDragWin || capWin;
318
+ if (w) return [Math.min(w.a,w.b), Math.max(w.a,w.b)];
319
+ return [viewMin, viewMax]; }
320
+ // Contiguous runs of points on the chosen side of the line within the capture
321
+ // window, bridging short gaps.
308
322
  function thRuns() { const val=thEff(); if (val===null) return [];
309
323
  const dir=thdirEl.value, gapMax=Math.max(0, parseInt(thgapEl.value)||0);
324
+ const win=capRange(), lo=win[0], hi=win[1];
310
325
  const runs=[]; let s=null, e=null, gap=0;
311
- for (let i=0;i<N;i++) { const p=pts[i];
326
+ for (let i=0;i<N;i++) { const p=pts[i]; if (p.ts<lo || p.ts>hi) continue;
312
327
  const q = p.v!==null && (dir==='above' ? p.v>val : p.v<val);
313
328
  if (q) { if (s===null) s=p.ts; e=p.ts; gap=0; }
314
329
  else if (s!==null) { gap++; if (gap>gapMax) { runs.push([s,e]); s=null; gap=0; } } }
@@ -328,8 +343,8 @@ function addCaptured(a,b) {
328
343
  if (host===null) incidents.push({a, b, label:''}); }
329
344
  function toggleTh(on) { thMode = (on===undefined) ? !thMode : !!on;
330
345
  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(); }
346
+ if (thMode) { hover=null; } else { thHover=null; thDown=null; thDragWin=null; capWin=null; updateThWin(); }
347
+ c.style.cursor = thMode ? 'crosshair' : 'crosshair'; if (thMode) { updateThWin(); thCount(); } draw(); }
333
348
 
334
349
  function rr(g,x,y,w,h,r) { g.beginPath();
335
350
  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 +391,20 @@ function draw() {
376
391
  if (dragging && dragging.mode==='new') { const x0=px(dragging.a), x1=px(dragging.b);
377
392
  ctx.fillStyle='rgba(240,173,78,0.28)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); }
378
393
  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([]); } }
394
+ if (thMode) {
395
+ const win=capRange(), narrow = !!(thDragWin || capWin);
396
+ 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);
397
+ if (narrow) { // dim everything outside the capture window
398
+ ctx.fillStyle='rgba(27,25,22,0.5)';
399
+ ctx.fillRect(M.l*dpr, M.t*dpr, xlo-M.l*dpr, plotH());
400
+ ctx.fillRect(xhi, M.t*dpr, (c.width-M.r*dpr)-xhi, plotH());
401
+ ctx.strokeStyle='rgba(240,173,78,0.7)'; ctx.lineWidth=1*dpr; ctx.setLineDash([3*dpr,3*dpr]);
402
+ ctx.beginPath(); ctx.moveTo(xlo,M.t*dpr); ctx.lineTo(xlo,c.height-M.b*dpr);
403
+ ctx.moveTo(xhi,M.t*dpr); ctx.lineTo(xhi,c.height-M.b*dpr); ctx.stroke(); ctx.setLineDash([]); }
404
+ const val=thEff();
405
+ if (val!==null && val>=vmin && val<=vmax) { const yy=py(val);
406
+ ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1.5*dpr; ctx.setLineDash([6*dpr,4*dpr]);
407
+ ctx.beginPath(); ctx.moveTo(xlo, yy); ctx.lineTo(xhi, yy); ctx.stroke(); ctx.setLineDash([]); } }
382
408
  ctx.restore();
383
409
  if (thMode) drawThLabel();
384
410
  else if (dragging && !ovAct) drawDragLabel();
@@ -389,8 +415,9 @@ function draw() {
389
415
  // would be captured.
390
416
  function drawThLabel() {
391
417
  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');
418
+ const n=thRuns().length, win=capRange(), narrow = !!(thDragWin || capWin);
419
+ const text='line '+fmtVal(val)+' · '+thdirEl.value+' · '+n+' span'+(n===1?'':'s')
420
+ + (narrow ? (' · '+fmtDur(win[1]-win[0])+' window') : '');
394
421
  ctx.font=(11*dpr)+'px ui-monospace, monospace';
395
422
  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
423
  ctx.fillStyle='rgba(27,25,22,0.96)'; ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1*dpr;
@@ -491,7 +518,9 @@ function hitIncident(clientX, clientY) {
491
518
  return null;
492
519
  }
493
520
  c.addEventListener('mousedown', e => {
494
- if (thMode) { thvalEl.value = String(Math.round(vAt(e.clientY)*1000)/1000); thCount(); draw(); return; }
521
+ // In threshold mode a press either sets the line (a click) or paints a capture
522
+ // window (a horizontal drag) — resolved on mouseup by how far it moved.
523
+ if (thMode) { thDown = {x:e.clientX, ts:tsAt(e.clientX)}; thHover = vAt(e.clientY); thCount(); draw(); return; }
495
524
  const hit = hitIncident(e.clientX, e.clientY), t = tsAt(e.clientX);
496
525
  if (hit && hit.edge==='del') { removeIncident(incidents[hit.i]); return; }
497
526
  if (hit && hit.edge==='move') { const iv=incidents[hit.i]; selObj=iv;
@@ -501,7 +530,10 @@ c.addEventListener('mousedown', e => {
501
530
  else { selObj=null; dragging={mode:'new', a:t, b:t, sx:e.clientX, cx:e.clientX}; draw(); }
502
531
  });
503
532
  c.addEventListener('mousemove', e => { if (ovAct) return;
504
- if (thMode && !dragging) { thHover=vAt(e.clientY); thCount(); draw(); return; }
533
+ if (thMode && !dragging) {
534
+ if (thDown && Math.abs(e.clientX - thDown.x) > 6) { thDragWin = {a: thDown.ts, b: tsAt(e.clientX)}; }
535
+ else { if (thDown) thDragWin = null; thHover = vAt(e.clientY); }
536
+ thCount(); draw(); return; }
505
537
  if (dragging) {
506
538
  dragging.cx=e.clientX; const t=tsAt(e.clientX);
507
539
  if (dragging.mode==='new') { dragging.b=t; }
@@ -539,8 +571,16 @@ window.addEventListener('mousemove', e => { if (!ovAct) return; const t=ovTsAtCs
539
571
  if (ovAct.type==='l') setView(Math.min(t, viewMax-minSpan), viewMax);
540
572
  else if (ovAct.type==='r') setView(viewMin, Math.max(t, viewMin+minSpan));
541
573
  else { const d=t-ovAct.grab; setView(ovAct.vMin+d, ovAct.vMax+d); } });
542
- window.addEventListener('mouseup', () => {
574
+ window.addEventListener('mouseup', e => {
543
575
  if (ovAct) { ovAct=null; return; }
576
+ if (thMode && thDown) {
577
+ if (Math.abs(e.clientX - thDown.x) > 6) { // a drag → set the capture window
578
+ const a=thDown.ts, b=tsAt(e.clientX); capWin = {a:Math.min(a,b), b:Math.max(a,b)};
579
+ } else { // a click → set the threshold line value
580
+ thvalEl.value = String(Math.round(vAt(e.clientY)*1000)/1000);
581
+ }
582
+ thDown=null; thDragWin=null; updateThWin(); thCount(); draw(); return;
583
+ }
544
584
  if (!dragging) return;
545
585
  if (dragging.mode==='new') {
546
586
  if (Math.abs(dragging.cx-dragging.sx) > 4) {
@@ -574,7 +614,9 @@ window.focusInc = i => { const iv=incidents[i]; if (!iv) return; selObj=iv;
574
614
  const thbtnEl=document.getElementById('thbtn'), thbarEl=document.getElementById('thbar');
575
615
  const thvalEl=document.getElementById('thval'), thdirEl=document.getElementById('thdir');
576
616
  const thgapEl=document.getElementById('thgap'), thaddEl=document.getElementById('thadd');
577
- const thdoneEl=document.getElementById('thdone');
617
+ const thdoneEl=document.getElementById('thdone'), thwinEl=document.getElementById('thwin');
618
+ function updateThWin() { thwinEl.style.display = capWin ? '' : 'none'; }
619
+ thwinEl.onclick = () => { capWin=null; updateThWin(); thCount(); draw(); };
578
620
  thbtnEl.onclick = () => toggleTh();
579
621
  thdoneEl.onclick = () => toggleTh(false);
580
622
  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.0
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