detectkit 0.31.0__tar.gz → 0.32.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 (139) hide show
  1. {detectkit-0.31.0/detectkit.egg-info → detectkit-0.32.0}/PKG-INFO +1 -1
  2. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/html_labeler.py +22 -3
  4. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/CLAUDE.section.md +5 -1
  5. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/cli.md +4 -3
  6. detectkit-0.32.0/detectkit/tuning/assets/tune.js +80 -0
  7. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/tuning/config_writer.py +4 -3
  8. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/tuning/payload.py +28 -4
  9. {detectkit-0.31.0 → detectkit-0.32.0/detectkit.egg-info}/PKG-INFO +1 -1
  10. detectkit-0.31.0/detectkit/tuning/assets/tune.js +0 -80
  11. {detectkit-0.31.0 → detectkit-0.32.0}/LICENSE +0 -0
  12. {detectkit-0.31.0 → detectkit-0.32.0}/MANIFEST.in +0 -0
  13. {detectkit-0.31.0 → detectkit-0.32.0}/README.md +0 -0
  14. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/__init__.py +0 -0
  15. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/__init__.py +0 -0
  16. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/base.py +0 -0
  17. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/branding.py +0 -0
  18. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/email.py +0 -0
  19. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/factory.py +0 -0
  20. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/mattermost.py +0 -0
  21. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/slack.py +0 -0
  22. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/telegram.py +0 -0
  23. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/channels/webhook.py +0 -0
  24. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  25. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  26. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  27. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  28. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  29. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  30. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  31. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  32. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  33. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/__init__.py +0 -0
  34. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/_base.py +0 -0
  35. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/_types.py +0 -0
  36. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/autotuner.py +0 -0
  37. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/config_emitter.py +0 -0
  38. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/crossval.py +0 -0
  39. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/detector_select.py +0 -0
  40. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/distribution.py +0 -0
  41. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/grid_search.py +0 -0
  42. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/label_server.py +0 -0
  43. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/labels.py +0 -0
  44. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/result.py +0 -0
  45. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/scoring.py +0 -0
  46. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/seasonality_search.py +0 -0
  47. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/settings.py +0 -0
  48. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/autotune/window_select.py +0 -0
  49. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/__init__.py +0 -0
  50. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/_output.py +0 -0
  51. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  52. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  53. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  54. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  55. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  56. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  57. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  58. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  59. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  60. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  61. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/__init__.py +0 -0
  62. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/autotune.py +0 -0
  63. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/clean.py +0 -0
  64. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/init.py +0 -0
  65. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/init_claude.py +0 -0
  66. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/run.py +0 -0
  67. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/test_alert.py +0 -0
  68. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/tune.py +0 -0
  69. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/commands/unlock.py +0 -0
  70. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/cli/main.py +0 -0
  71. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/config/__init__.py +0 -0
  72. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/config/metric_config.py +0 -0
  73. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/config/profile.py +0 -0
  74. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/config/project_config.py +0 -0
  75. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/config/validator.py +0 -0
  76. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/core/__init__.py +0 -0
  77. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/core/interval.py +0 -0
  78. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/core/models.py +0 -0
  79. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/__init__.py +0 -0
  80. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/_sql_manager.py +0 -0
  81. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/clickhouse_manager.py +0 -0
  82. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/__init__.py +0 -0
  83. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  84. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  85. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_base.py +0 -0
  86. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  87. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_detections.py +0 -0
  88. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  89. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  90. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_schema.py +0 -0
  91. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  92. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/internal_tables/manager.py +0 -0
  93. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/manager.py +0 -0
  94. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/mysql_manager.py +0 -0
  95. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/postgres_manager.py +0 -0
  96. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/database/tables.py +0 -0
  97. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/__init__.py +0 -0
  98. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/base.py +0 -0
  99. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/factory.py +0 -0
  100. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/seasonality.py +0 -0
  101. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/__init__.py +0 -0
  102. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  103. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/iqr.py +0 -0
  104. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/mad.py +0 -0
  105. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  106. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/detectors/statistical/zscore.py +0 -0
  107. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/loaders/__init__.py +0 -0
  108. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/loaders/metric_loader.py +0 -0
  109. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/loaders/query_template.py +0 -0
  110. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/__init__.py +0 -0
  111. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/error_dispatch.py +0 -0
  112. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  113. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  114. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  115. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  116. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  117. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  118. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  119. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/reporting/__init__.py +0 -0
  120. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/reporting/assets/report.js +0 -0
  121. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/reporting/builder.py +0 -0
  122. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/reporting/html_report.py +0 -0
  123. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/tuning/__init__.py +0 -0
  124. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/tuning/html.py +0 -0
  125. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/tuning/server.py +0 -0
  126. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/utils/__init__.py +0 -0
  127. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/utils/datetime_utils.py +0 -0
  128. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/utils/env_interpolation.py +0 -0
  129. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/utils/json_utils.py +0 -0
  130. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit/utils/stats.py +0 -0
  131. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit.egg-info/SOURCES.txt +0 -0
  132. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit.egg-info/dependency_links.txt +0 -0
  133. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit.egg-info/entry_points.txt +0 -0
  134. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit.egg-info/requires.txt +0 -0
  135. {detectkit-0.31.0 → detectkit-0.32.0}/detectkit.egg-info/top_level.txt +0 -0
  136. {detectkit-0.31.0 → detectkit-0.32.0}/pyproject.toml +0 -0
  137. {detectkit-0.31.0 → detectkit-0.32.0}/requirements.txt +0 -0
  138. {detectkit-0.31.0 → detectkit-0.32.0}/setup.cfg +0 -0
  139. {detectkit-0.31.0 → detectkit-0.32.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.31.0
3
+ Version: 0.32.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.31.0"
7
+ __version__ = "0.32.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -295,7 +295,11 @@ const TICK_STEPS=[MS_S,2*MS_S,5*MS_S,10*MS_S,15*MS_S,30*MS_S,
295
295
  MS_H,2*MS_H,3*MS_H,6*MS_H,12*MS_H, MS_D,2*MS_D,3*MS_D,5*MS_D,7*MS_D,14*MS_D];
296
296
  function niceTimeTicks(lo, hi, target) {
297
297
  const span=Math.max(hi-lo, 1);
298
- if (span > target*28*MS_D) { // month/year calendar ticks for long spans
298
+ // Escalate to calendar months/years once even the coarsest sub-monthly step
299
+ // (14d) would exceed the target count (span > target*14d). The old threshold
300
+ // (target*28d) left a gap where no sub-monthly step fit and the loop fell
301
+ // through to a 14d step, packing ~13 labels in and overlapping.
302
+ if (span > target*14*MS_D) { // month/year calendar ticks for long spans
299
303
  const mSteps=[1,2,3,6,12,24,36,60,120,240]; let stepM=mSteps[mSteps.length-1];
300
304
  for (const m of mSteps) { if (span/(m*30.44*MS_D) <= target) { stepM=m; break; } }
301
305
  const d=new Date(lo); let y=d.getUTCFullYear(), mo=d.getUTCMonth();
@@ -322,6 +326,17 @@ const fmtAxis = (ts, step) => { const s=new Date(ts).toISOString();
322
326
  if (step >= MS_D) return s.slice(5,10);
323
327
  if (step >= MS_H) return s.slice(5,16).replace('T',' ');
324
328
  return s.slice(5,19).replace('T',' '); };
329
+ // Which ticks may show a TEXT label without colliding: keep the first, then keep
330
+ // each only if its x clears the previous kept label by its width + a gutter.
331
+ // Gridlines/ticks still draw for every entry; only text is thinned. Measures with
332
+ // the passed context's CURRENT font, so set it before calling.
333
+ function labelMask(g, ticks, xOf, step) {
334
+ const show=new Array(ticks.length).fill(false); if (!ticks.length) return show;
335
+ const minGap=g.measureText(fmtAxis(ticks[0], step)).width + 16*dpr; let lastX=-Infinity;
336
+ for (let i=0;i<ticks.length;i++) { const xx=xOf(ticks[i]);
337
+ if (xx-lastX >= minGap) { show[i]=true; lastX=xx; } }
338
+ return show;
339
+ };
325
340
  function fmtDur(ms) { const m = Math.round(ms/60000); if (m<60) return m+'m';
326
341
  const h = Math.floor(m/60), mm = m%60; if (h<24) return h+'h'+(mm?(' '+mm+'m'):'');
327
342
  const d = Math.floor(h/24), hh = h%24; return d+'d'+(hh?(' '+hh+'h'):''); }
@@ -439,9 +454,11 @@ function draw() {
439
454
  // in real time reads off the grid (not only by chasing the cursor).
440
455
  ctx.textBaseline = 'top';
441
456
  const xtk=niceTimeTicks(viewMin, viewMax, 7);
442
- xtk.ticks.forEach(ts => { const xx=px(ts);
457
+ const showXL=labelMask(ctx, xtk.ticks, px, xtk.step);
458
+ xtk.ticks.forEach((ts,ti) => { const xx=px(ts);
443
459
  ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=1*dpr;
444
460
  ctx.beginPath(); ctx.moveTo(xx, M.t*dpr); ctx.lineTo(xx, c.height-M.b*dpr); ctx.stroke();
461
+ if (!showXL[ti]) return;
445
462
  ctx.fillStyle='#6e675b';
446
463
  ctx.textAlign = xx < (M.l+24)*dpr ? 'left' : xx > c.width-(M.r+24)*dpr ? 'right' : 'center';
447
464
  ctx.fillText(fmtAxis(ts, xtk.step), xx, c.height-(M.b-8)*dpr); });
@@ -578,9 +595,11 @@ function drawOverview() {
578
595
  // absolute-time anchors under the strip, so a point's real time is readable
579
596
  // across the whole series without hovering.
580
597
  octx.font=(10*dpr)+'px ui-monospace, monospace'; octx.textBaseline='top';
581
- otk.ticks.forEach(ts => { const xx=ovpx(ts);
598
+ const showOL=labelMask(octx, otk.ticks, ovpx, otk.step);
599
+ otk.ticks.forEach((ts,ti) => { const xx=ovpx(ts);
582
600
  octx.strokeStyle='rgba(201,194,180,0.25)'; octx.lineWidth=1*dpr;
583
601
  octx.beginPath(); octx.moveTo(xx, plotBot); octx.lineTo(xx, plotBot+3*dpr); octx.stroke();
602
+ if (!showOL[ti]) return;
584
603
  octx.fillStyle='#6e675b';
585
604
  octx.textAlign = xx < (OM.l+26)*dpr ? 'left' : xx > ov.width-(OM.r+26)*dpr ? 'right' : 'center';
586
605
  octx.fillText(fmtAxis(ts, otk.step), xx, plotBot+5*dpr); });
@@ -37,6 +37,7 @@ version — **read the relevant one on demand** instead of guessing:
37
37
  | Choosing/tuning detectors, preprocessing, trends, seasonality | `.claude/rules/detectkit/detectors.md` |
38
38
  | Alert rules (quorum/direction/consecutive), cooldown, recovery, templates | `.claude/rules/detectkit/alerting.md` |
39
39
  | Auto-tuning a detector (`dtk autotune`), labels file, the `autotune:` block, `_dtk_autotune_runs` | `.claude/rules/detectkit/autotune.md` |
40
+ | Manually/interactively tuning a detector in the browser and writing it back in place (`dtk tune`) | `.claude/rules/detectkit/cli.md` |
40
41
 
41
42
  ### Skills
42
43
 
@@ -48,7 +49,10 @@ version — **read the relevant one on demand** instead of guessing:
48
49
  to a YAML file that validates and is ready to run.
49
50
  - **Tune / pick a detector, or build an alert from scratch and tune it** — use
50
51
  the **`dtk-autotune`** skill; it gathers seasonality + incidents, runs
51
- `dtk autotune`, and explains the chosen, annotated config.
52
+ `dtk autotune`, and explains the chosen, annotated config (an automatic,
53
+ cross-validated search). For **hands-on** tuning instead, `dtk tune` opens a
54
+ browser view where you drag the detector's knobs against the real series and
55
+ write the result back in place (see `cli.md`).
52
56
  - **Hit a detectkit bug, or have feedback** — once you've ruled out a local
53
57
  config fix (see the gotchas below), use the **`dtk-feedback`** skill to file a
54
58
  redacted bug report, feature request, or comment as a GitHub issue on the
@@ -77,9 +77,10 @@ Full reference: `autotune.md`.
77
77
 
78
78
  The **manual, interactive** sibling of `dtk autotune`. Opens a localhost browser
79
79
  view of the metric's **real** persisted series and lets you turn the detector's
80
- knobs (type, threshold, window, recency weighting + half-life, detrend, smoothing,
81
- **seasonality groups**, alert `consecutive_anomalies`) while the confidence band
82
- and flagged anomalies **recompute live**. The chart is **zoomable** (scroll/drag +
80
+ knobs (type including **Manual bounds** with lower/upper sliders — threshold,
81
+ window, recency weighting + half-life, detrend, smoothing, **seasonality groups**,
82
+ **direction** (both/up/down), alert `consecutive_anomalies`) while the confidence
83
+ band and flagged anomalies **recompute live**. The chart is **zoomable** (scroll/drag +
83
84
  a navigator strip) and a **"Points shown"** slider trims the active sample to speed
84
85
  up recompute on a long metric. Clicking **Apply** writes the chosen
85
86
  config back into the metric YAML **in place** (autotune, by contrast, writes a new
@@ -0,0 +1,80 @@
1
+ "use strict";(()=>{var mt={"--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 V(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||mt[t]||"#888"}function ft(t){let n=t.replace("#","").trim();n.length===3&&(n=n[0]+n[0]+n[1]+n[1]+n[2]+n[2]);let a=parseInt(n,16);return n.length!==6||Number.isNaN(a)?[209,91,54]:[a>>16&255,a>>8&255,a&255]}function O(t,n){let[a,e,d]=ft(t);return`rgba(${a},${e},${d},${n})`}function Ze(t){let n=Math.max(1,window.devicePixelRatio||1),a=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(a*n),t.height=Math.round(e*n),n}var je=Number.isFinite;function Ue(t,n,a,e,d,o,b,s,f,g,h,M){let j=n.length,C=Math.max(1,Math.round(b)),D=d-e||1,ue=0;for(let R=0;R<j;R++){let q=a[R];!je(q)||n[R]<e||n[R]>d||ue++}if(t.strokeStyle=g,t.lineWidth=h*M,t.lineJoin="round",t.beginPath(),ue<=C){let R=!1;for(let q=0;q<j;q++){let Y=a[q],W=n[q];if(!je(Y)||W<e||W>d){R=!1;continue}let P=s(W),J=f(Y);R?t.lineTo(P,J):(t.moveTo(P,J),R=!0)}}else{let R=new Array(C).fill(null),q=new Array(C).fill(null);for(let W=0;W<j;W++){let P=a[W],J=n[W];if(!je(P)||J<e||J>d)continue;let L=Math.floor((J-e)/D*(C-1));L=L<0?0:L>C-1?C-1:L,(R[L]===null||P<R[L])&&(R[L]=P),(q[L]===null||P>q[L])&&(q[L]=P)}let Y=!1;for(let W=0;W<C;W++){if(q[W]===null){Y=!1;continue}let P=o+W,J=f(q[W]),L=f(R[W]);Y?t.lineTo(P,J):(t.moveTo(P,J),Y=!0),t.lineTo(P,L)}}t.stroke()}function et(t){let n=Math.abs(t);return n>=1e3?t.toFixed(0):n>=10?t.toFixed(1):n>=1?t.toFixed(2):t.toFixed(3)}function tt(t,n){let a=new Date(t).toISOString();return n<2*864e5?a.slice(5,16).replace("T"," "):a.slice(5,10)}function nt(t,n,a){return{left:n.l*a,top:n.t*a,right:t.width-n.r*a,bottom:t.height-n.b*a}}function ot(t,n,a,e,d,o,b){let s=nt(n,a,e),f=Math.max(s.left,Math.min(d(o),s.right));f<=s.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(s.left,s.top,f-s.left,s.bottom-s.top),t.strokeStyle=O(V("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(f,s.top),t.lineTo(f,s.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=O(V("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(b,f+6*e,s.top+5*e),t.restore())}function rt(t,n,a,e,d,o,b){let s=nt(n,a,e),f=5*e;t.save();for(let g=0;g<o.length;g++){let h=o[g],M=d(h.t);if(M<s.left-1||M>s.right+1)continue;let j=b(h.kind);t.strokeStyle=O(j,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(M,s.top),t.lineTo(M,s.bottom),t.stroke(),t.fillStyle=j,t.beginPath(),t.moveTo(M-f,s.top),t.lineTo(M+f,s.top),t.lineTo(M,s.top+f*1.4),t.closePath(),t.fill()}t.restore()}var pt={mad:1,zscore:2,iqr:4};function ht(t,n){let a=t.seasonalityData;if(!a||a.length===0)return 0;let e=0;for(let d of n){let o=new Set;for(let b of a)o.add(d.map(s=>{var f;return String((f=b==null?void 0:b[s])!=null?f:"")}).join("|"));e=Math.max(e,o.size)}return e}function it(t,n){let a=t.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,a);let e=Math.max(n.minSamples,pt[n.type]);if(n.smoothing==="sma"&&(e=Math.max(e,n.smoothingWindow-1)),n.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(e=Math.max(e,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let o=ht(t,n.seasonalityComponents);if(o>0){let b=n.minSamplesPerGroup*o;n.windowSize>=b&&(e=Math.max(e,b))}}return Math.min(e,a)}var T={l:52,r:14,t:14,b:26},Ve=46,st=10,bt=13,re=Number.isFinite,we=1e3,ye=60*we,ke=60*ye,X=24*ke,Ye=[we,2*we,5*we,10*we,15*we,30*we,ye,2*ye,5*ye,10*ye,15*ye,30*ye,ke,2*ke,3*ke,6*ke,12*ke,X,2*X,3*X,5*X,7*X,14*X];function lt(t,n,a){let e=Math.max(n-t,1);if(e>a*14*X){let s=[1,2,3,6,12,24,36,60,120,240],f=s[s.length-1];for(let D of s)if(e/(D*30.44*X)<=a){f=D;break}let g=new Date(t),h=g.getUTCFullYear(),M=g.getUTCMonth();if(f>=12){let D=f/12;h=Math.floor(h/D)*D,M=0}else M=Math.floor(M/f)*f;let j=[],C=Date.UTC(h,M,1);for(;C<=n;)C>=t&&j.push(C),M+=f,h+=Math.floor(M/12),M%=12,C=Date.UTC(h,M,1);return{ticks:j,step:f*30*X}}let d=Ye[Ye.length-1];for(let s of Ye)if(e/s<=a){d=s;break}let o;if(d%X===0){let s=new Date(t);o=Date.UTC(s.getUTCFullYear(),s.getUTCMonth(),s.getUTCDate());let f=d/X,g=Math.round(o/X);o+=(f-g%f)%f*X}else o=Math.ceil(t/d)*d;let b=[];for(let s=o;s<=n;s+=d)s>=t&&b.push(s);return{ticks:b,step:d}}function Ge(t,n){let a=new Date(t).toISOString();return n>=320*X?a.slice(0,4):n>=26*X?a.slice(0,7):n>=X?a.slice(5,10):n>=ke?a.slice(5,16).replace("T"," "):a.slice(5,19).replace("T"," ")}function at(t,n={}){let a=t.getContext("2d");if(!a)throw new Error("chart: 2D context unavailable");let e=a,d=!!n.navigable,o=1,b=null,s=-1,f=0,g=0,h=1,M=0,j=1,C=0,D=1,ue="";function R(){o=Ze(t)}let q=()=>d?(st+Ve)*o:0,Y=()=>t.width-(T.l+T.r)*o,W=()=>t.height-(T.t+T.b)*o-q(),P=()=>d?C:g,J=()=>d?D:h,L=()=>J()-P()||1,le=()=>h-g||1,H=i=>T.l*o+(i-P())/L()*Y(),ne=i=>t.height-T.b*o-q()-(i-M)/(j-M||1)*W(),de=()=>t.height-Ve*o,Z=()=>(Ve-bt)*o,F=()=>t.width-(T.l+T.r)*o,z=i=>T.l*o+(i-g)/le()*F(),Le=i=>de()+Z()-(i-M)/(j-M||1)*Z(),Se=(i,l,m)=>{let u=new Array(i.length).fill(!1);if(i.length===0)return u;let p=e.measureText(Ge(i[0],m)).width+16*o,w=-1/0;for(let S=0;S<i.length;S++){let _=l(i[S]);_-w>=p&&(u[S]=!0,w=_)}return u},ae=(i,l,m)=>Math.max(l,Math.min(m,i)),ee=()=>{let i=b==null?void 0:b.series,l=i?i.timestamps.length:0,m=l>1?le()/(l-1):1e3;return Math.max(m*8,1e3)};function oe(i,l){let m=l-i,u=ee();if(m<u){let p=(i+l)/2;i=p-u/2,l=p+u/2,m=u}m>=le()&&(i=g,l=h),i<g&&(l+=g-i,i=g),l>h&&(i-=l-h,l=h),C=ae(i,g,h),D=ae(l,g,h),ce()}let Ie=i=>{let l=(i-T.l*o)/(Y()||1);return P()+ae(l,0,1)*L()},ie=i=>{let l=(i-T.l*o)/(F()||1);return g+ae(l,0,1)*le()};function Be(i){let l=b==null?void 0:b.series;if(!l||l.timestamps.length===0)return-1;let m=(i-T.l*o)/(Y()||1),u=P()+m*L(),p=l.timestamps,w=0,S=p.length-1;for(;w<S;){let _=w+S>>1;p[_]<u?w=_+1:S=_}return w>0&&u-p[w-1]<p[w]-u&&(w-=1),w}function me(i,l){let m=i.timestamps;if(g=m[0],h=m[m.length-1],d){let S=`${m.length}:${g}:${h}`;S!==ue?(ue=S,C=g,D=h):(C=ae(C,g,h),D=ae(Math.max(D,C+ee()),g,h))}let u=1/0,p=-1/0;for(let S of i.values)re(S)&&(S<u&&(u=S),S>p&&(p=S));for(let S of l)S.scored&&(re(S.lower)&&S.lower<u&&(u=S.lower),re(S.upper)&&S.upper>p&&(p=S.upper));(!re(u)||!re(p))&&(u=0,p=1),p<=u&&(p=u+1);let w=(p-u)*.06;M=u-w,j=p+w}function be(i,l,m){let u=b.series.timestamps;Ue(e,u,i,P(),J(),T.l*o,Y(),H,ne,l,m,o)}function Oe(i,l){let m=[],u=-1;for(let p=Math.max(0,l);p<i.length;p++){let w=i[p];w.scored&&re(w.lower)&&re(w.upper)?u===-1&&(u=p):u!==-1&&(m.push([u,p-1]),u=-1)}return u!==-1&&m.push([u,i.length-1]),m}function ge(){if(f=0,!b||t.width===0||t.height===0)return;let{series:i,scored:l,params:m,alerts:u}=b;if(i.timestamps.length===0){e.fillStyle=V("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let p=V("--clay"),w=V("--st-anomaly"),S=V("--faint"),_=V("--muted");e.fillStyle=V("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*o}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let x=0;x<=4;x++){let y=M+(j-M)*x/4,k=ne(y);e.strokeStyle=O(S,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(T.l*o,k),e.lineTo(t.width-T.r*o,k),e.stroke(),e.fillStyle=_,e.textAlign="right",e.fillText(et(y),(T.l-8)*o,k)}e.textBaseline="top";let U=t.height-T.b*o-q();if(d){let x=lt(P(),J(),7),y=Se(x.ticks,H,x.step);for(let k=0;k<x.ticks.length;k++){let r=x.ticks[k],c=H(r);e.strokeStyle=O(S,.1),e.lineWidth=1*o,e.beginPath(),e.moveTo(c,T.t*o),e.lineTo(c,U),e.stroke(),y[k]&&(e.fillStyle=_,e.textAlign=c<(T.l+24)*o?"left":c>t.width-(T.r+24)*o?"right":"center",e.fillText(Ge(r,x.step),c,U+7*o))}}else{let x=L();for(let y=0;y<=5;y++){let k=g+x*y/5,r=H(k);e.fillStyle=_,e.textAlign=y===0?"left":y===5?"right":"center",e.fillText(tt(k,x),r,U+7*o)}}e.save(),e.beginPath(),e.rect(T.l*o,T.t*o,Y(),W()),e.clip();let I=l.length,E=Math.min(it(i,m),I),$=E<I?i.timestamps[E]:void 0,B=Oe(l,E);e.fillStyle=O(p,.13);for(let[x,y]of B){e.beginPath(),e.moveTo(H(l[x].timestamp),ne(l[x].upper));for(let k=x+1;k<=y;k++)e.lineTo(H(l[k].timestamp),ne(l[k].upper));for(let k=y;k>=x;k--)e.lineTo(H(l[k].timestamp),ne(l[k].lower));e.closePath(),e.fill()}e.strokeStyle=O(p,.4),e.lineWidth=1*o;for(let[x,y]of B)for(let k of["upper","lower"]){e.beginPath();for(let r=x;r<=y;r++){let c=H(l[r].timestamp),A=ne(l[r][k]);r===x?e.moveTo(c,A):e.lineTo(c,A)}e.stroke()}e.strokeStyle=O(S,.55),e.lineWidth=1*o,e.setLineDash([3*o,3*o]);for(let[x,y]of B){e.beginPath();for(let k=x;k<=y;k++){let r=l[k].center;if(!re(r))continue;let c=H(l[k].timestamp),A=ne(r);k===x?e.moveTo(c,A):e.lineTo(c,A)}e.stroke()}if(e.setLineDash([]),m.smoothing!=="none"){be(i.values,O(p,.28),1.25);let x=l.map(y=>y.processedValue);be(x,p,1.6)}else be(i.values,p,1.5);for(let x=E;x<I;x++){let y=l[x];if(!y.scored||!re(y.value))continue;let k=H(y.timestamp),r=ne(y.value);y.isAnomaly?(e.fillStyle=O(w,.18),e.beginPath(),e.arc(k,r,6*o,0,Math.PI*2),e.fill(),e.fillStyle=w,e.beginPath(),e.arc(k,r,3*o,0,Math.PI*2),e.fill()):i.truthAnomaly[x]&&(e.strokeStyle=O(_,.7),e.lineWidth=1.25*o,e.beginPath(),e.arc(k,r,3.5*o,0,Math.PI*2),e.stroke())}$!==void 0&&ot(e,t,T,o,H,$,"detection at full power \u2192"),s>=0&&s<l.length&&Me(s,m.windowSize,l,i,S),u&&u.length&&rt(e,t,T,o,H,u,x=>x==="anomaly"?V("--st-anomaly"):x==="recovery"?V("--st-recovery"):V("--st-nodata")),e.restore(),d&&_e(i,u,p,S,_)}function _e(i,l,m,u,p){let w=de(),S=w+Z(),_=T.l*o,U=t.width-T.r*o,I=lt(g,h,5);e.save(),e.beginPath(),e.rect(_,w,F(),Z()),e.clip(),e.strokeStyle=O(u,.1),e.lineWidth=1*o;for(let y of I.ticks){let k=z(y);e.beginPath(),e.moveTo(k,w),e.lineTo(k,S),e.stroke()}Ue(e,i.timestamps,i.values,g,h,_,F(),z,Le,O(m,.7),1.1,o),e.restore();let E=z(C),$=z(D);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(_,w,E-_,Z()),e.fillRect($,w,U-$,Z()),l&&l.length){let y=V("--st-anomaly");e.fillStyle=O(y,.85);for(let k of l){let r=z(k.t);e.fillRect(r-1*o,w,2*o,Z())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(E,w,$-E,Z()),e.strokeStyle=m,e.lineWidth=1.5*o,e.strokeRect(E,w+1,$-E,Z()-2),e.fillStyle=m;let B=w+Z()/2-8*o;e.fillRect(E-2*o,B,4*o,16*o),e.fillRect($-2*o,B,4*o,16*o),e.font=`${10*o}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=p;let x=Se(I.ticks,z,I.step);for(let y=0;y<I.ticks.length;y++){let k=I.ticks[y],r=z(k);e.strokeStyle=O(u,.25),e.lineWidth=1*o,e.beginPath(),e.moveTo(r,S),e.lineTo(r,S+3*o),e.stroke(),x[y]&&(e.textAlign=r<(T.l+26)*o?"left":r>t.width-(T.r+26)*o?"right":"center",e.fillText(Ge(k,I.step),r,S+5*o))}}function Me(i,l,m,u,p){let w=u.timestamps,S=Math.max(0,i-l),_=i-1,U=T.t*o,I=W();if(_>=S){let B=u.intervalSeconds*1e3/L()*Y()*.5,x=H(w[S])-B,y=H(w[_])+B;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(x,U,y-x,I),e.strokeStyle=O(p,.5),e.lineWidth=1*o,e.beginPath(),e.moveTo(x,U),e.lineTo(x,U+I),e.moveTo(y,U),e.lineTo(y,U+I),e.stroke()}let E=H(w[i]);e.strokeStyle=O(p,.85),e.lineWidth=1*o,e.setLineDash([2*o,2*o]),e.beginPath(),e.moveTo(E,U),e.lineTo(E,U+I),e.stroke(),e.setLineDash([]);let $=m[i];if($.scored&&re($.lower)&&re($.upper)){let B=5*o;e.strokeStyle=O(V("--clay"),.85),e.lineWidth=1.5*o;for(let x of[$.lower,$.upper]){let y=ne(x);e.beginPath(),e.moveTo(E-B,y),e.lineTo(E+B,y),e.stroke()}}if(re($.value)){let B=ne($.value);e.fillStyle=V("--term-bg"),e.beginPath(),e.arc(E,B,4*o,0,Math.PI*2),e.fill(),e.strokeStyle=$.isAnomaly?V("--st-anomaly"):V("--clay"),e.lineWidth=2*o,e.beginPath(),e.arc(E,B,4*o,0,Math.PI*2),e.stroke()}}function ce(){f===0&&(f=requestAnimationFrame(ge))}function Ce(){var m;if(!n.onHover||!b)return;if(s<0){n.onHover(null);return}let i=(m=b.scored[s])!=null?m:null,l={index:s,point:i,windowStart:Math.max(0,s-b.params.windowSize),windowEnd:s-1};n.onHover(l)}let te=null,G=null,fe=i=>{let l=t.getBoundingClientRect();return{x:(i.clientX-l.left)*o,y:(i.clientY-l.top)*o}},Ne=i=>d&&i>=de(),K=i=>{let l=z(C),m=z(D),u=8*o;return Math.abs(i-l)<=u?"l":Math.abs(i-m)<=u?"r":i>l&&i<m?"move":"out"};function ve(i){if(!b)return;let{x:l,y:m}=fe(i);if(te||G)return;if(Ne(m)){s!==-1&&(s=-1,Ce(),ce());let p=K(l);t.style.cursor=p==="l"||p==="r"?"ew-resize":p==="move"?"grab":"pointer";return}d&&(t.style.cursor="grab");let u=Be(l);u!==s&&(s=u,Ce(),ce())}function Te(){s!==-1&&(s=-1,Ce(),ce())}function Ae(i){if(!d||!b)return;let{x:l,y:m}=fe(i);if(m>=de()){let u=K(l);if(u==="l")G={type:"l",grab:0,vMin:C,vMax:D};else if(u==="r")G={type:"r",grab:0,vMin:C,vMax:D};else if(u==="move")G={type:"move",grab:ie(l),vMin:C,vMax:D};else{let p=ie(l),w=L();oe(p-w/2,p+w/2),G={type:"move",grab:p,vMin:C,vMax:D}}t.style.cursor="grabbing",i.preventDefault()}else m>T.t*o&&m<de()-st*o&&(te={x:l,vMin:C,vMax:D},t.style.cursor="grabbing",i.preventDefault())}function pe(i){if(!b)return;let{x:l}=fe(i);if(te){let m=(l-te.x)*(te.vMax-te.vMin)/(Y()||1);oe(te.vMin-m,te.vMax-m)}else if(G){let m=ie(l);G.type==="l"?oe(Math.min(m,D-ee()),D):G.type==="r"?oe(C,Math.max(m,C+ee())):oe(G.vMin+(m-G.grab),G.vMax+(m-G.grab))}}function Ee(){(te||G)&&(te=null,G=null,t.style.cursor="grab")}function We(i){if(!d||!b)return;i.preventDefault();let{x:l,y:m}=fe(i),u=ae(L()*Math.pow(1.0015,i.deltaY),ee(),le());if(m>=de()){let p=ie(l);oe(p-u/2,p+u/2)}else{let p=Ie(l),w=(p-C)/(L()||1);oe(p-w*u,p-w*u+u)}}function qe(){d&&oe(g,h)}t.addEventListener("mousemove",ve),t.addEventListener("mouseleave",Te),d&&(t.addEventListener("mousedown",Ae),t.addEventListener("wheel",We,{passive:!1}),t.addEventListener("dblclick",qe),window.addEventListener("mousemove",pe),window.addEventListener("mouseup",Ee));function Re(i){b=i,me(i.series,i.scored),s>=i.series.timestamps.length&&(s=-1),ce()}function $e(){R(),b&&(me(b.series,b.scored),ce())}function xe(){t.removeEventListener("mousemove",ve),t.removeEventListener("mouseleave",Te),d&&(t.removeEventListener("mousedown",Ae),t.removeEventListener("wheel",We),t.removeEventListener("dblclick",qe),window.removeEventListener("mousemove",pe),window.removeEventListener("mouseup",Ee)),f!==0&&cancelAnimationFrame(f),f=0,b=null}return R(),{render:Re,resize:$e,destroy:xe}}var gt={mad:3,zscore:3,iqr:1.5},vt={mad:10,zscore:3,iqr:4},xt="dtk-tune";function v(t,n,a){let e=document.createElement(t);return n&&(e.className=n),a!=null&&(e.textContent=a),e}function Fe(t,n){let a=v("label","dtk-ctl-label",t);if(n){a.title=n;let e=v("span","dtk-ctl-info","\u24D8");e.title=n,a.appendChild(document.createTextNode(" ")),a.appendChild(e)}return a}function He(t,n,a,e,d){let o=v("div","dtk-ctl");o.appendChild(Fe(t,d));let b=v("div","dtk-seg"),s=a,f=[],g=()=>{f.forEach(h=>h.classList.toggle("on",h.dataset.v===s))};return n.forEach(h=>{let M=v("button","dtk-seg-btn",h.label);M.type="button",M.dataset.v=h.value,M.onclick=()=>{s=h.value,g(),e(s)},f.push(M),b.appendChild(M)}),g(),o.appendChild(b),{row:o,get:()=>s,set:h=>{s=h,g()}}}function Pe(t,n,a){var g;let e=v("div","dtk-ctl"),d=v("div","dtk-ctl-head"),o=Fe(t,n.hint),b=v("span","dtk-ctl-val"),s=(g=n.fmt)!=null?g:(h=>String(h));d.appendChild(o),d.appendChild(b),e.appendChild(d);let f=v("input","dtk-range");return f.type="range",f.min=String(n.min),f.max=String(n.max),f.step=String(n.step),f.value=String(n.value),b.textContent=s(n.value),f.oninput=()=>{let h=Number(f.value);b.textContent=s(h),a(h)},e.appendChild(f),{row:e,get:()=>Number(f.value),setMax:h=>{f.max=String(h),Number(f.value)>h&&(f.value=String(h),b.textContent=s(h))}}}function ct(t){if(t.type==="manual_bounds"){let a={};return t.lowerBound!=null&&(a.lower_bound=t.lowerBound),t.upperBound!=null&&(a.upper_bound=t.upperBound),t.inputType!=="values"&&(a.input_type=t.inputType),a}let n={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(n.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(n.half_life=t.halfLife)),t.detrend!=="none"&&(n.detrend=t.detrend),t.smoothing!=="none"&&(n.smoothing=t.smoothing),t.inputType!=="values"&&(n.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(n.seasonality_components=t.seasonalityComponents,n.min_samples_per_group=t.minSamplesPerGroup),n}function wt(t,n){let a=ct(t),e=[`type: ${t.type}`];for(let[d,o]of Object.entries(a))e.push(`${d}=${typeof o=="object"?JSON.stringify(o):o}`);return t.direction&&t.direction!=="any"&&e.push(`direction=${t.direction}`),e.push(`consecutive_anomalies=${n}`),e.join(" \xB7 ")}function yt(t,n){var x,y,k;Mt(),n.classList.add(xt),n.innerHTML="";let a=v("div","dtk-tune-root");n.appendChild(a);let e=t.points.length,d={timestamps:t.points.map(r=>r.t),values:t.points.map(r=>r.v==null?NaN:r.v),intervalSeconds:t.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:t.seasonality_columns.length?t.seasonality:void 0,seasonalityColumns:t.seasonality_columns.length?t.seasonality_columns:void 0},o=d,b=r=>{let c=Math.max(0,e-r);return c<=0?d:{timestamps:d.timestamps.slice(c),values:d.values.slice(c),intervalSeconds:d.intervalSeconds,truthAnomaly:d.truthAnomaly.slice(c),seasonalityData:d.seasonalityData?d.seasonalityData.slice(c):void 0,seasonalityColumns:d.seasonalityColumns}},s=t.detector,f=t.consecutive_anomalies,g=d.values.filter(r=>Number.isFinite(r)).sort((r,c)=>r-c),h=g.length?g[0]:0,M=g.length?g[g.length-1]:1,j=r=>g.length?g[Math.min(g.length-1,Math.max(0,Math.round(r*(g.length-1))))]:0,C=Math.max((M-h)*.05,1e-9),D=h-C,ue=M+C,R=Math.max((ue-D)/400,1e-9),q=s.lowerBound!=null?s.lowerBound:j(.05),Y=s.upperBound!=null?s.upperBound:j(.95),W=(x=s.seasonalityComponents)!=null?x:[],P=new Map;W.forEach((r,c)=>r.forEach(A=>P.set(A,c+1)));let J=()=>{let r=0;P.forEach(A=>{A>r&&(r=A)});let c=[];for(let A=1;A<=r;A++){let Q=t.seasonality_columns.filter(N=>P.get(N)===A);Q.length&&c.push(Q)}return c.length?c:null},L=()=>{var r;return{type:ve.get(),threshold:pe.get(),windowSize:$e.get(),minSamples:s.minSamples,inputType:s.inputType,smoothing:u.get(),smoothingAlpha:s.smoothingAlpha,smoothingWindow:s.smoothingWindow,windowWeights:xe.get(),halfLife:xe.get()==="exponential"?i.get():null,detrend:m.get(),seasonalityComponents:J(),minSamplesPerGroup:(r=vt[ve.get()])!=null?r:s.minSamplesPerGroup,consecutiveAnomalies:f,direction:w.get(),lowerBound:Te.get(),upperBound:Ae.get()}},le=v("div","dtk-tune-header"),H=v("div","dtk-tune-titlerow");H.appendChild(v("h1","dtk-tune-title",t.metric));let ne=v("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");H.appendChild(ne),le.appendChild(H);let de=t.project?`${t.project} \xB7 `:"";le.appendChild(v("div","dtk-tune-sub",`${de}${e} points \xB7 ${St(t.interval_seconds)} grid`)),t.description&&le.appendChild(v("div","dtk-tune-desc",t.description)),a.appendChild(le);let Z=v("div","dtk-tune-grid"),F=v("div","dtk-tune-controls"),z=v("div","dtk-tune-main");Z.appendChild(F),Z.appendChild(z),a.appendChild(Z);let Le=v("div","dtk-tune-trim"),Se=v("div","dtk-tune-trim-head");Se.appendChild(Fe("Points shown","Trim the active sample to the most-recent N points. Fewer points recompute faster (cost grows with points \xD7 window) and make a shorter period easier to read \u2014 handy once you can see a smaller window/period is enough."));let ae=v("span","dtk-tune-trim-val");Se.appendChild(ae),Le.appendChild(Se);let ee=v("input","dtk-range");ee.type="range",ee.min=String(Math.min(e,200)),ee.max=String(e),ee.step=String(Math.max(1,Math.round(e/200))),ee.value=String(e),Le.appendChild(ee),z.appendChild(Le);let oe=v("div","dtk-tune-chart"),Ie=v("canvas");oe.appendChild(Ie);let ie=v("div","dtk-tune-spin");ie.appendChild(v("span","dtk-spin-ring")),ie.appendChild(v("span","dtk-spin-txt","computing\u2026")),oe.appendChild(ie),z.appendChild(oe);let Be=v("div","dtk-tune-legend"),me=(r,c,A)=>{let Q=v("span","dtk-leg-item");Q.title=A,Q.appendChild(v("span",`dtk-leg-sw ${r}`)),Q.appendChild(v("span","dtk-leg-txt",c)),Be.appendChild(Q)};me("line","metric","The metric value over time."),me("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),me("center","band center","The expected value at the middle of the band."),me("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),me("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),z.appendChild(Be);let be=v("div","dtk-tune-readout");z.appendChild(be);let Oe=at(Ie,{navigable:!0,onHover:r=>{if(!r||!r.point||!r.point.scored){be.textContent="";return}let c=r.point;be.textContent=`t=${kt(c.timestamp)} value=${ze(c.value)} band=[${ze(c.lower)}, ${ze(c.upper)}]`+(c.isAnomaly?` \u26A0 ${c.direction} (sev ${c.severity.toFixed(2)})`:"")}}),ge=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function C(e){let n=0;for(let t of e)n+=t;return n}function un(e){let n=e.map((t,o)=>o);return n.sort((t,o)=>e[t]<e[o]?-1:e[t]>e[o]?1:t-o),n}function cn(e,n,t){let o=n.length;if(o===0)return NaN;if(e<=n[0])return t[0];if(e>=n[o-1])return t[o-1];for(let i=1;i<o;i++)if(e<=n[i]){let r=n[i-1],s=n[i],c=t[i-1],u=t[i];if(s===r)return c;let d=(e-r)/(s-r);return c+d*(u-c)}return t[o-1]}function j(e,n,t){let o=un(e),i=o.map(b=>e[b]),r=o.map(b=>n[b]),s=C(r),c=new Array(r.length),u=0;for(let b=0;b<r.length;b++)u+=r[b],c[b]=(u-.5*r[b])/s;let d=t/100;return cn(d,c,i)}function P(e,n){return j(e,n,50)}function ln(e,n,t){let o=t===void 0?P(e,n):t,i=e.map(r=>Math.abs(r-o));return P(i,n)}function mn(e,n){let t=C(n),o=0;for(let i=0;i<e.length;i++)o+=e[i]*(n[i]/t);return o}function an(e,n,t,o=0){let i=C(n),r=n.map(u=>u/i),s=t===void 0?C(e.map((u,d)=>u*r[d])):t,c=0;for(let u=0;u<e.length;u++)c+=r[u]*(e[u]-s)**2;if(o===1){let u=1-C(r.map(d=>d*d));u>1e-12&&(c/=u)}return Math.sqrt(c)}function fn(e,n){return n.smoothing==="none"?e.slice():n.smoothing==="ema"?dn(e,n.smoothingAlpha):bn(e,n.smoothingWindow)}function dn(e,n){let t=e.length,o=new Array(t).fill(NaN);if(t===0)return o;let i=-1;for(let r=0;r<t;r++)if(!Number.isNaN(e[r])){i=r;break}if(i===-1)return o;o[i]=e[i];for(let r=i+1;r<t;r++)Number.isNaN(e[r])?o[r]=o[r-1]:o[r]=n*e[r]+(1-n)*o[r-1];return o}function bn(e,n){let t=e.length,o=new Array(t).fill(NaN);for(let i=0;i<t;i++){let r=Math.max(0,i-n+1),s=0,c=0;for(let u=r;u<=i;u++)Number.isNaN(e[u])||(s+=e[u],c+=1);o[i]=c>0?s/c:NaN}return o}function Y(e,n){let t=e.length;if(n.inputType==="values")return e.slice();let o=new Array(t).fill(NaN);for(let i=1;i<t;i++){let r=e[i-1],s=e[i];n.inputType==="changes"?o[i]=(s-r)/r:n.inputType==="absolute_changes"?o[i]=s-r:o[i]=Math.log(s+1)-Math.log(r+1)}return o}function pn(e,n){return e.halfLife!==null?e.halfLife:Math.max(e.windowSize/20,n/2,1)}function yn(e,n){if(e.windowWeights==="none")return null;let t=e.windowSize,o=new Array(t);if(e.windowWeights==="exponential"){let i=pn(e,n);for(let r=1;r<=t;r++){let s=Math.min(r/i,1e3);o[r-1]=Math.pow(.5,s)}return o}for(let i=1;i<=t;i++)o[i-1]=(t+1-i)/t;return o}function Q(e,n){return n===null?e.map(()=>1):e.map(t=>n[t-1])}function gn(e){let n=e[0];for(let t of e)t>n&&(n=t);return n}function hn(e){let n=e[0];for(let t of e)t<n&&(n=t);return n}function Sn(e,n,t){if(e.length<4)return 0;let o=(gn(n)+hn(n))/2,i=n.map(a=>a>o),r=i.map(a=>!a),s=0,c=0;for(let a=0;a<n.length;a++)i[a]?s++:c++;if(s<2||c<2)return 0;let u=(a,M)=>M.filter((x,w)=>a[w]),d=u(r,e),b=u(i,e),N=u(r,n),y=u(i,n),h=u(r,t),m=u(i,t),A=P(d,h),p=P(b,m),S=P(N,h),g=P(y,m);return g===S?0:(A-p)/(g-S)}var wn={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function U(e,n,t){if(e==="mad"){let r=P(n,t);return{median:r,mad:ln(n,t,r)}}if(e==="zscore"){let r=mn(n,t);return{mean:r,std:an(n,t,r,1)}}let o=j(n,t,25),i=j(n,t,75);return{q1:o,q3:i,iqr:i-o}}function Nn(e,n,t){if(e==="mad"){if(n.mad===0)return[n.median-1e-10,n.median+1e-10];let o=t*1.4826*n.mad;return[n.median-o,n.median+o]}return e==="zscore"?n.std===0?[n.mean-1e-10,n.mean+1e-10]:[n.mean-t*n.std,n.mean+t*n.std]:n.iqr===0?[n.q1-1e-10,n.q3+1e-10]:[n.q1-t*n.iqr,n.q3+t*n.iqr]}function An(e,n,t){if(e==="mad"){let o=1.4826*n.mad;return o>0?t/o:1/0}return e==="zscore"?n.std>0?t/n.std:1/0:n.iqr>0?t/n.iqr:1/0}function Mn(e,n){return e==="mad"?n.median:e==="zscore"?n.mean:(n.q1+n.q3)/2}function F(e,n,t,o,i){return{index:e,timestamp:n,value:t,processedValue:o,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Dn(e,n){var d,b,N;let{timestamps:t,values:o}=e,i=t.length,r=Y(o,n),s=(d=n.lowerBound)!=null?d:null,c=(b=n.upperBound)!=null?b:null,u=[];for(let y=0;y<i;y++){let h=o[y],m=r[y],A=t[y];if(Number.isNaN(m)){u.push(F(y,A,h,m,"missing_data"));continue}let p=!1,S=null,g=0;s!==null&&m<s&&(p=!0,S="below",g=s-m),c!==null&&m>c&&(p=!0,S="above",g=m-c);let a=0;if(p)if(s!==null&&c!==null){let v=c-s;a=v>0?g/v:1/0}else a=g;let M=s!=null?s:-1/0,x=c!=null?c:1/0,w=s!==null&&c!==null?(s+c)/2:(N=s!=null?s:c)!=null?N:NaN;u.push({index:y,timestamp:A,value:h,processedValue:m,scored:!0,isAnomaly:p,lower:M,upper:x,center:w,direction:S,severity:a,reason:"ok"})}return u}function Z(e,n){if(n.type==="manual_bounds")return Dn(e,n);let t=n.type,o=Math.max(n.minSamples,X[t]),i=wn[t],{timestamps:r,values:s}=e,c=r.length,u=fn(s,n),d=Y(u,n),b=n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0,N=e.seasonalityData,y=yn(n,o),h=[];for(let m=0;m<c;m++){let A=s[m],p=d[m],S=r[m];if(Number.isNaN(p)){h.push(F(m,S,A,p,"missing_data"));continue}let g=Math.max(0,m-n.windowSize),a=m-g,M=d.slice(g,m),x=M.map(l=>!Number.isNaN(l)),w=[];for(let l=0;l<M.length;l++)x[l]&&w.push(M[l]);if(w.length<o){h.push(F(m,S,A,p,"insufficient_data"));continue}let v=new Array(a);for(let l=0;l<a;l++)v[l]=a-l;let L=[];for(let l=0;l<a;l++)x[l]&&L.push(v[l]);let B=Q(L,y),W=0;n.detrend==="linear"&&(W=Sn(w,L,B));let nn=W!==0?w.map((l,R)=>l+W*L[R]):w,G=U(t,nn,B),_={...G};if(b&&N){let l=N[m];for(let R of n.seasonalityComponents){let K=new Array(a).fill(!0);for(let f=0;f<a;f++){let q=N[g+f],T=!0;for(let V of R)if(!q||q[V]!==(l==null?void 0:l[V])){T=!1;break}K[f]=T}let z=[],E=[];for(let f=0;f<a;f++)x[f]&&K[f]&&(z.push(M[f]),E.push(v[f]));if(z.length<n.minSamplesPerGroup)continue;let tn=Q(E,y),on=W!==0?z.map((f,q)=>f+W*E[q]):z,rn=U(t,on,tn);for(let[f,q]of i){let T=G[f],sn=(q==="spread"?T>0:T!==0)?rn[f]/T:1;_[f]*=sn}}}let[D,k]=Nn(t,_,n.threshold);D>k&&([D,k]=[k,D]);let H=p<D||p>k,O=null,J=0;if(H){let l;p<D?(O="below",l=D-p):(O="above",l=p-k),J=An(t,_,l)}let en=Mn(t,_);h.push({index:m,timestamp:S,value:A,processedValue:p,scored:!0,isAnomaly:H,lower:D,upper:k,center:en,direction:O,severity:J,reason:"ok"})}return h}function Pn(e,n){let t=e.seasonalityData;if(!t||t.length===0)return 0;let o=0;for(let i of n){let r=new Set;for(let s of t)r.add(i.map(c=>{var u;return String((u=s==null?void 0:s[c])!=null?u:"")}).join("|"));o=Math.max(o,r.size)}return o}function $(e,n){let t=e.timestamps.length;if(n.type==="manual_bounds")return Math.min(n.inputType!=="values"?1:0,t);let o=Math.max(n.minSamples,X[n.type]);if(n.smoothing==="sma"&&(o=Math.max(o,n.smoothingWindow-1)),n.smoothing==="ema"&&(o=Math.max(o,Math.ceil(5/n.smoothingAlpha))),n.inputType!=="values"&&(o=Math.max(o,1)),n.seasonalityComponents!==null&&n.seasonalityComponents.length>0&&Array.isArray(e.seasonalityData)&&e.seasonalityData.length>0){let r=Pn(e,n.seasonalityComponents);if(r>0){let s=n.minSamplesPerGroup*r;n.windowSize>=s&&(o=Math.max(o,s))}}return Math.min(o,t)}var I=null;function xn(e,n){if(!n||n==="any")return;let t=n==="up"?"above":"below";for(let o of e)o.isAnomaly&&o.direction!==t&&(o.isAnomaly=!1)}function vn(e,n,t){let o=[],i=0;for(let r=0;r<e.length;r++){if(!(e[r].scored&&e[r].isAnomaly)){i=0;continue}let c=r>0&&e[r-1].scored&&e[r-1].isAnomaly,u=r>0&&e[r].timestamp-e[r-1].timestamp===n;i=c&&u?i+1:1,i===t&&o.push(r)}return o}self.onmessage=e=>{let n=e.data;if(n.type==="series"){I=n.series;return}if(n.type==="run"&&I){let t=n.params,o=Z(I,t);xn(o,t.direction);let i=I.intervalSeconds*1e3,r=vn(o,i,t.consecutiveAnomalies),s=$(I,t),c=0;for(let u of o)u.scored&&u.isAnomaly&&c++;self.postMessage({type:"result",id:n.id,scored:o,fires:r,eff:s,flagged:c})}};})();
2
+ `],{type:"text/javascript"})));ge.postMessage({type:"series",series:o});let _e=0,Me=null;ge.onmessage=r=>{let c=r.data;if(c.type!=="result"||c.id!==_e||!Me)return;ie.classList.remove("on");let A=Me,Q=c.fires.map(N=>({t:o.timestamps[N],kind:"anomaly"}));Oe.render({series:o,scored:c.scored,params:A,alerts:Q}),I.textContent=`${c.flagged} flagged \xB7 ${c.fires.length} alert${c.fires.length===1?"":"s"} \xB7 warm-up ${c.eff} pts`,$.textContent=wt(A,f)},ge.onerror=()=>{ie.classList.remove("on"),I.textContent="recompute failed \u2014 see the browser console"};let ce=()=>{Me=L(),_e+=1,ie.classList.add("on"),ge.postMessage({type:"run",id:_e,params:Me})},Ce=r=>r>1?Ke(d.timestamps[e-1]-d.timestamps[e-r]):"\u2014",te=r=>{ae.textContent=r>=e?`${e} pts \xB7 full (${Ce(e)})`:`${r} pts \xB7 ${Ce(r)}`};te(e);function G(r){o=b(r),ge.postMessage({type:"series",series:o}),K()}let fe=0;ee.oninput=()=>{let r=Number(ee.value);te(r),fe&&window.clearTimeout(fe),fe=window.setTimeout(()=>G(r),200)};let Ne=0,K=()=>{Ne&&window.clearTimeout(Ne),Ne=window.setTimeout(ce,130)},ve=He("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"},{label:"Manual",value:"manual_bounds"}],s.type,r=>{var c;qe((c=gt[r])!=null?c:3),U(),K()},"The statistic for the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles) \u2014 all windowed \u2014 or Manual (fixed lower/upper thresholds, no window/history).");F.appendChild(ve.row);let Te=Pe("Lower bound",{min:D,max:ue,step:R,value:q,fmt:r=>ze(r),hint:"Manual bounds: values below this read as anomalous. Drag in from the data range to see how many points fall outside (and how many alerts that yields)."},K);F.appendChild(Te.row);let Ae=Pe("Upper bound",{min:D,max:ue,step:R,value:Y,fmt:r=>ze(r),hint:"Manual bounds: values above this read as anomalous."},K);F.appendChild(Ae.row);let pe=Pe("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:s.threshold,fmt:r=>r.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},K);F.appendChild(pe.row);let Ee=pe.row.querySelector("input"),We=pe.row.querySelector(".dtk-ctl-val"),qe=r=>{Ee&&(Ee.value=String(r)),We&&(We.textContent=r.toFixed(1))},Re=Math.max(50,Math.min(2e3,Math.floor(e/2))),$e=Pe("Window size (points)",{min:10,max:Re,step:5,value:Math.min(s.windowSize,Re),fmt:r=>`${r} \xB7 ${Ke(r*t.interval_seconds*1e3)}`,hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},K);F.appendChild($e.row);let xe=He("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],s.windowWeights,()=>{U(),K()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");F.appendChild(xe.row);let i=Pe("Half-life (points)",{min:1,max:Re,step:1,value:(y=s.halfLife)!=null?y:Math.max(5,Math.round(s.windowSize/20)),fmt:r=>`${r} \xB7 ${Ke(r*t.interval_seconds*1e3)}`,hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},K),l=i.row;l.style.display=s.windowWeights==="exponential"?"":"none",F.appendChild(l);let m=He("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],s.detrend,K,"Remove a robust linear trend from each window before computing the band, so a steadily rising/falling metric is not flagged for the trend itself.");F.appendChild(m.row);let u=He("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],s.smoothing,K,"Smooth the series before detection (EMA or SMA) so single-point jitter does not flag. The detector judges the smoothed line; the raw values show as a faint ghost.");F.appendChild(u.row);let p=null;if(t.seasonality_columns.length){let r=t.seasonality_columns,c=v("div","dtk-ctl");p=c,c.appendChild(Fe("Seasonality groups","Condition the band on seasonal keys. Pick a group per column: columns in the SAME group are combined into one key (e.g. dow\xD7hour); separate groups each apply their own correction. Off = ignore that column."));let A=Math.min(r.length,6),Q=[{label:"\u2014",value:"0"}];for(let N=1;N<=A;N++)Q.push({label:`G${N}`,value:String(N)});r.forEach(N=>{let he=v("div","dtk-season-row");he.appendChild(v("span","dtk-season-col",N));let Xe=v("div","dtk-seg dtk-season-seg"),Je=[],dt=()=>{var se;return(se=P.get(N))!=null?se:0},Qe=()=>Je.forEach(se=>se.classList.toggle("on",Number(se.dataset.v)===dt()));Q.forEach(se=>{let De=v("button","dtk-seg-btn",se.label);De.type="button",De.dataset.v=se.value,De.title=se.value==="0"?`ignore ${N}`:`put ${N} in group ${se.value}`,De.onclick=()=>{P.set(N,Number(se.value)),Qe(),K()},Je.push(De),Xe.appendChild(De)}),Qe(),he.appendChild(Xe),c.appendChild(he)}),F.appendChild(c)}let w=He("Direction",[{label:"both",value:"any"},{label:"up",value:"up"},{label:"down",value:"down"}],(k=t.direction)!=null?k:"any",K,"Which anomalies to show and count toward alerts: both directions, only spikes ABOVE the band (up) or only drops BELOW it (down). A preview filter mirroring the alert direction policy \u2014 it never changes the band itself.");F.appendChild(w.row);let S=Pe("Alert: consecutive anomalies",{min:1,max:10,step:1,value:f,fmt:r=>String(r),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},r=>{f=r,K()});F.appendChild(S.row);let _=[pe.row,$e.row,xe.row,m.row,u.row];p&&_.push(p);function U(){let r=ve.get()==="manual_bounds";for(let c of _)c.style.display=r?"none":"";Te.row.style.display=r?"":"none",Ae.row.style.display=r?"":"none",l.style.display=!r&&xe.get()==="exponential"?"":"none"}U();let I=v("div","dtk-tune-stat");z.appendChild(I);let E=v("div","dtk-tune-cfg");E.appendChild(v("span","dtk-tune-cfg-k","// effective config"));let $=v("code","dtk-tune-cfg-v");if(E.appendChild($),z.appendChild(E),t.save_url){let r=v("div","dtk-tune-apply"),c=v("button","dtk-apply-btn","Apply to metric");c.type="button",c.title="Write this detector config back into the metric YAML (the previous version is archived under metrics/.history/). Trimming the sample does not change what is written.";let A=v("span","dtk-apply-msg");c.onclick=()=>{let Q=L();c.disabled=!0,A.className="dtk-apply-msg info",A.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:Q.type,params:ct(Q)},consecutive_anomalies:f})}).then(N=>N.ok?N.json():N.text().then(he=>{throw new Error(he||`HTTP ${N.status}`)})).then(N=>{var he;A.className="dtk-apply-msg ok",A.textContent=`Applied \u2192 ${(he=N.saved)!=null?he:"metric"} (previous archived). You can close this tab.`}).catch(N=>{c.disabled=!1,A.className="dtk-apply-msg err",A.textContent=`Apply failed: ${N.message}`})},r.appendChild(c),r.appendChild(A),z.appendChild(r)}else z.appendChild(v("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));ce();let B=0;window.addEventListener("resize",()=>{B&&cancelAnimationFrame(B),B=requestAnimationFrame(()=>Oe.resize())})}function ze(t){if(!Number.isFinite(t))return"\u2014";let n=Math.abs(t);return n!==0&&(n<.01||n>=1e6)?t.toExponential(2):Number(t.toFixed(n<1?4:2)).toString()}function kt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function Ke(t){let n=Math.round(t/6e4);if(n<60)return`${n}m`;let a=Math.floor(n/60);if(a<24)return`${a}h`;let e=Math.floor(a/24),d=a%24;return d?`${e}d ${d}h`:`${e}d`}function St(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var ut=!1;function Mt(){if(ut)return;ut=!0;let t=`
3
+ .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
4
+ --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
5
+ --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
6
+ --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
7
+ .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
8
+ .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
9
+ .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
10
+ .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
11
+ color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
12
+ .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
13
+ .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
14
+ .dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
15
+ @media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
16
+ .dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
17
+ border:1px solid var(--border);border-radius:12px;padding:16px;}
18
+ .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
19
+ .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
20
+ .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
21
+ .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
22
+ .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
23
+ .dtk-seg.dtk-wrap{flex-wrap:wrap;}
24
+ .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
25
+ font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
26
+ .dtk-seg-btn:hover{color:var(--ink);}
27
+ .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
28
+ .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
29
+ .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
30
+ .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
31
+ .dtk-tune-chart{position:relative;width:100%;height:470px;background:var(--surface);
32
+ border:1px solid var(--border);border-radius:12px;overflow:hidden;}
33
+ .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
34
+ .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
35
+ .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
36
+ .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
37
+ font-size:12px;overflow-x:auto;}
38
+ .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
39
+ .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
40
+ .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
41
+ .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
42
+ font-size:14px;font-weight:600;cursor:pointer;}
43
+ .dtk-apply-btn:hover{background:var(--c7);}
44
+ .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
45
+ .dtk-apply-msg{font-size:13px;}
46
+ .dtk-apply-msg.ok{color:var(--green);}
47
+ .dtk-apply-msg.err{color:var(--anom);}
48
+ .dtk-apply-msg.info{color:var(--muted);}
49
+ .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
50
+ border-radius:8px;padding:10px 12px;}
51
+ .dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
52
+ .dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
53
+ border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
54
+ .dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
55
+ .dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
56
+ .dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
57
+ background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
58
+ padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
59
+ .dtk-tune-spin.on{display:inline-flex;}
60
+ .dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
61
+ border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
62
+ @keyframes dtk-spin{to{transform:rotate(360deg);}}
63
+ .dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
64
+ .dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
65
+ .dtk-leg-sw{display:inline-block;flex:0 0 auto;}
66
+ .dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
67
+ .dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
68
+ border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
69
+ .dtk-leg-sw.center{width:16px;height:2px;
70
+ background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
71
+ .dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
72
+ .dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
73
+ border-top:7px solid var(--anom);}
74
+ .dtk-leg-txt{white-space:nowrap;}
75
+ .dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
76
+ .dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
77
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
78
+ .dtk-season-seg{flex:0 0 auto;padding:2px;}
79
+ .dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
80
+ `,n=document.createElement("style");n.textContent=t,document.head.appendChild(n)}window.__DTK_TUNE__={render:yt};})();
@@ -20,9 +20,10 @@ import yaml
20
20
  from detectkit.config.metric_config import MetricConfig
21
21
  from detectkit.detectors.factory import DetectorFactory
22
22
 
23
- # The detector types the interactive tuner can emit (windowed statistical). Other
24
- # types (e.g. manual_bounds) need domain thresholds, not interactive tuning.
25
- _TUNABLE_TYPES = {"mad", "zscore", "iqr"}
23
+ # The detector types the interactive tuner can emit: the windowed statistical
24
+ # detectors plus the stateless manual_bounds (lower/upper threshold) detector,
25
+ # whose bounds the tuner lets you drag against the real series.
26
+ _TUNABLE_TYPES = {"mad", "zscore", "iqr", "manual_bounds"}
26
27
 
27
28
  _RULE = "# " + "─" * 61
28
29
 
@@ -43,7 +43,9 @@ def default_window_points(seed_window: int) -> int:
43
43
  _THRESHOLD_DEFAULT = {"mad": 3.0, "zscore": 3.0, "iqr": 1.5}
44
44
  # Per-type min-samples-per-group defaults (mirror the detector subclasses).
45
45
  _MIN_SAMPLES_PER_GROUP_DEFAULT = {"mad": 10, "zscore": 3, "iqr": 4}
46
- _TUNABLE_TYPES = ("mad", "zscore", "iqr")
46
+ # Detector types the interactive tuner can seed/emit: the windowed statistical
47
+ # detectors plus the stateless manual_bounds (lower/upper threshold) detector.
48
+ _TUNABLE_TYPES = ("mad", "zscore", "iqr", "manual_bounds")
47
49
 
48
50
 
49
51
  def _normalize_seasonality_components(
@@ -65,9 +67,12 @@ def _normalize_seasonality_components(
65
67
  def _seed_detector(metric_config: MetricConfig) -> dict[str, Any]:
66
68
  """Map the metric's current detector config to the TS ``DetectorParams`` shape.
67
69
 
68
- Picks the first tunable (mad/zscore/iqr) detector to seed the sliders; falls
69
- back to MAD defaults when the metric has none (e.g. only ``manual_bounds``).
70
- Param names are camelCased to match the client port.
70
+ Picks the first tunable (mad/zscore/iqr/manual_bounds) detector to seed the
71
+ sliders; falls back to MAD defaults when the metric has none. Param names are
72
+ camelCased to match the client port. The windowed knobs always carry sane
73
+ defaults (even for a manual_bounds seed) so switching detector type in the UI
74
+ never hits an empty slider; ``lowerBound``/``upperBound`` carry the manual
75
+ thresholds (``None`` for a windowed metric — the client picks a data default).
71
76
  """
72
77
  chosen = next(
73
78
  (d for d in metric_config.detectors if d.type in _TUNABLE_TYPES),
@@ -99,10 +104,26 @@ def _seed_detector(metric_config: MetricConfig) -> dict[str, Any]:
99
104
  "minSamplesPerGroup": params.get(
100
105
  "min_samples_per_group", _MIN_SAMPLES_PER_GROUP_DEFAULT.get(dtype, 10)
101
106
  ),
107
+ # manual_bounds thresholds (None for a windowed metric).
108
+ "lowerBound": params.get("lower_bound"),
109
+ "upperBound": params.get("upper_bound"),
102
110
  }
103
111
  return seed
104
112
 
105
113
 
114
+ def _seed_direction(metric_config: MetricConfig) -> str:
115
+ """Seed the tuner's direction view-filter from the first alerting block.
116
+
117
+ The tuner offers ``any``/``up``/``down``; the alerting policy may also be
118
+ ``same`` (a multi-detector agreement rule). Tuning is single-detector, so
119
+ ``same`` reads as ``any`` for the preview. Defaults to ``any``.
120
+ """
121
+ if not metric_config.alerting:
122
+ return "any"
123
+ raw = (metric_config.alerting[0].direction or "any").lower()
124
+ return raw if raw in ("up", "down", "any") else "any"
125
+
126
+
106
127
  def build_tune_payload(
107
128
  *,
108
129
  metric_config: MetricConfig,
@@ -141,6 +162,7 @@ def build_tune_payload(
141
162
  consecutive = 3
142
163
  if metric_config.alerting:
143
164
  consecutive = metric_config.alerting[0].consecutive_anomalies
165
+ direction = _seed_direction(metric_config)
144
166
 
145
167
  empty: dict[str, Any] = {
146
168
  "metric": name,
@@ -153,6 +175,7 @@ def build_tune_payload(
153
175
  "seasonality_columns": [],
154
176
  "detector": seed,
155
177
  "consecutive_anomalies": consecutive,
178
+ "direction": direction,
156
179
  "save_url": save_url,
157
180
  }
158
181
  if start is None or end is None:
@@ -190,5 +213,6 @@ def build_tune_payload(
190
213
  "seasonality_columns": season_cols,
191
214
  "detector": seed,
192
215
  "consecutive_anomalies": consecutive,
216
+ "direction": direction,
193
217
  "save_url": save_url,
194
218
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.31.0
3
+ Version: 0.32.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -1,80 +0,0 @@
1
- "use strict";(()=>{var tt={"--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 O(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()||tt[t]||"#888"}function nt(t){let r=t.replace("#","").trim();r.length===3&&(r=r[0]+r[0]+r[1]+r[1]+r[2]+r[2]);let u=parseInt(r,16);return r.length!==6||Number.isNaN(u)?[209,91,54]:[u>>16&255,u>>8&255,u&255]}function R(t,r){let[u,e,d]=nt(t);return`rgba(${u},${e},${d},${r})`}function Fe(t){let r=Math.max(1,window.devicePixelRatio||1),u=t.clientWidth||t.offsetWidth||0,e=t.clientHeight||t.offsetHeight||0;return t.width=Math.round(u*r),t.height=Math.round(e*r),r}var $e=Number.isFinite;function ze(t,r,u,e,d,i,p,s,m,x,f,k){let z=r.length,S=Math.max(1,Math.round(p)),A=d-e||1,ie=0;for(let W=0;W<z;W++){let N=u[W];!$e(N)||r[W]<e||r[W]>d||ie++}if(t.strokeStyle=x,t.lineWidth=f*k,t.lineJoin="round",t.beginPath(),ie<=S){let W=!1;for(let N=0;N<z;N++){let D=u[N],T=r[N];if(!$e(D)||T<e||T>d){W=!1;continue}let E=s(T),I=m(D);W?t.lineTo(E,I):(t.moveTo(E,I),W=!0)}}else{let W=new Array(S).fill(null),N=new Array(S).fill(null);for(let T=0;T<z;T++){let E=u[T],I=r[T];if(!$e(E)||I<e||I>d)continue;let P=Math.floor((I-e)/A*(S-1));P=P<0?0:P>S-1?S-1:P,(W[P]===null||E<W[P])&&(W[P]=E),(N[P]===null||E>N[P])&&(N[P]=E)}let D=!1;for(let T=0;T<S;T++){if(N[T]===null){D=!1;continue}let E=i+T,I=m(N[T]),P=m(W[T]);D?t.lineTo(E,I):(t.moveTo(E,I),D=!0),t.lineTo(E,P)}}t.stroke()}function Be(t){let r=Math.abs(t);return r>=1e3?t.toFixed(0):r>=10?t.toFixed(1):r>=1?t.toFixed(2):t.toFixed(3)}function je(t,r){let u=new Date(t).toISOString();return r<2*864e5?u.slice(5,16).replace("T"," "):u.slice(5,10)}function Ye(t,r,u){return{left:r.l*u,top:r.t*u,right:t.width-r.r*u,bottom:t.height-r.b*u}}function Ue(t,r,u,e,d,i,p){let s=Ye(r,u,e),m=Math.max(s.left,Math.min(d(i),s.right));m<=s.left+.5||(t.save(),t.fillStyle="rgba(17,15,13,0.42)",t.fillRect(s.left,s.top,m-s.left,s.bottom-s.top),t.strokeStyle=R(O("--faint"),.7),t.lineWidth=1*e,t.setLineDash([4*e,4*e]),t.beginPath(),t.moveTo(m,s.top),t.lineTo(m,s.bottom),t.stroke(),t.setLineDash([]),t.fillStyle=R(O("--faint"),.95),t.font=`${10*e}px ui-monospace, monospace`,t.textAlign="left",t.textBaseline="top",t.fillText(p,m+6*e,s.top+5*e),t.restore())}function Ge(t,r,u,e,d,i,p){let s=Ye(r,u,e),m=5*e;t.save();for(let x=0;x<i.length;x++){let f=i[x],k=d(f.t);if(k<s.left-1||k>s.right+1)continue;let z=p(f.kind);t.strokeStyle=R(z,.45),t.lineWidth=1*e,t.beginPath(),t.moveTo(k,s.top),t.lineTo(k,s.bottom),t.stroke(),t.fillStyle=z,t.beginPath(),t.moveTo(k-m,s.top),t.lineTo(k+m,s.top),t.lineTo(k,s.top+m*1.4),t.closePath(),t.fill()}t.restore()}var ot={mad:1,zscore:2,iqr:4};function rt(t,r){let u=t.seasonalityData;if(!u||u.length===0)return 0;let e=0;for(let d of r){let i=new Set;for(let p of u)i.add(d.map(s=>{var m;return String((m=p==null?void 0:p[s])!=null?m:"")}).join("|"));e=Math.max(e,i.size)}return e}function Ve(t,r){let u=t.timestamps.length,e=Math.max(r.minSamples,ot[r.type]);if(r.smoothing==="sma"&&(e=Math.max(e,r.smoothingWindow-1)),r.smoothing==="ema"&&(e=Math.max(e,Math.ceil(5/r.smoothingAlpha))),r.inputType!=="values"&&(e=Math.max(e,1)),r.seasonalityComponents!==null&&r.seasonalityComponents.length>0&&Array.isArray(t.seasonalityData)&&t.seasonalityData.length>0){let i=rt(t,r.seasonalityComponents);if(i>0){let p=r.minSamplesPerGroup*i;r.windowSize>=p&&(e=Math.max(e,p))}}return Math.min(e,u)}var C={l:52,r:14,t:14,b:26},Ie=46,Ke=10,it=13,te=Number.isFinite,ce=1e3,ue=60*ce,de=60*ue,q=24*de,Oe=[ce,2*ce,5*ce,10*ce,15*ce,30*ce,ue,2*ue,5*ue,10*ue,15*ue,30*ue,de,2*de,3*de,6*de,12*de,q,2*q,3*q,5*q,7*q,14*q];function Xe(t,r,u){let e=Math.max(r-t,1);if(e>u*28*q){let s=[1,2,3,6,12,24,36,60,120,240],m=s[s.length-1];for(let A of s)if(e/(A*30.44*q)<=u){m=A;break}let x=new Date(t),f=x.getUTCFullYear(),k=x.getUTCMonth();if(m>=12){let A=m/12;f=Math.floor(f/A)*A,k=0}else k=Math.floor(k/m)*m;let z=[],S=Date.UTC(f,k,1);for(;S<=r;)S>=t&&z.push(S),k+=m,f+=Math.floor(k/12),k%=12,S=Date.UTC(f,k,1);return{ticks:z,step:m*30*q}}let d=Oe[Oe.length-1];for(let s of Oe)if(e/s<=u){d=s;break}let i;if(d%q===0){let s=new Date(t);i=Date.UTC(s.getUTCFullYear(),s.getUTCMonth(),s.getUTCDate());let m=d/q,x=Math.round(i/q);i+=(m-x%m)%m*q}else i=Math.ceil(t/d)*d;let p=[];for(let s=i;s<=r;s+=d)s>=t&&p.push(s);return{ticks:p,step:d}}function Je(t,r){let u=new Date(t).toISOString();return r>=320*q?u.slice(0,4):r>=26*q?u.slice(0,7):r>=q?u.slice(5,10):r>=de?u.slice(5,16).replace("T"," "):u.slice(5,19).replace("T"," ")}function Qe(t,r={}){let u=t.getContext("2d");if(!u)throw new Error("chart: 2D context unavailable");let e=u,d=!!r.navigable,i=1,p=null,s=-1,m=0,x=0,f=1,k=0,z=1,S=0,A=1,ie="";function W(){i=Fe(t)}let N=()=>d?(Ke+Ie)*i:0,D=()=>t.width-(C.l+C.r)*i,T=()=>t.height-(C.t+C.b)*i-N(),E=()=>d?S:x,I=()=>d?A:f,P=()=>I()-E()||1,Y=()=>f-x||1,H=n=>C.l*i+(n-E())/P()*D(),G=n=>t.height-C.b*i-N()-(n-k)/(z-k||1)*T(),V=()=>t.height-Ie*i,K=()=>(Ie-it)*i,ne=()=>t.width-(C.l+C.r)*i,Q=n=>C.l*i+(n-x)/Y()*ne(),Ae=n=>V()+K()-(n-k)/(z-k||1)*K(),X=(n,o,a)=>Math.max(o,Math.min(a,n)),oe=()=>{let n=p==null?void 0:p.series,o=n?n.timestamps.length:0,a=o>1?Y()/(o-1):1e3;return Math.max(a*8,1e3)};function J(n,o){let a=o-n,c=oe();if(a<c){let l=(n+o)/2;n=l-c/2,o=l+c/2,a=c}a>=Y()&&(n=x,o=f),n<x&&(o+=x-n,n=x),o>f&&(n-=o-f,o=f),S=X(n,x,f),A=X(o,x,f),re()}let De=n=>{let o=(n-C.l*i)/(D()||1);return E()+X(o,0,1)*P()},se=n=>{let o=(n-C.l*i)/(ne()||1);return x+X(o,0,1)*Y()};function Pe(n){let o=p==null?void 0:p.series;if(!o||o.timestamps.length===0)return-1;let a=(n-C.l*i)/(D()||1),c=E()+a*P(),l=o.timestamps,h=0,y=l.length-1;for(;h<y;){let _=h+y>>1;l[_]<c?h=_+1:y=_}return h>0&&c-l[h-1]<l[h]-c&&(h-=1),h}function Le(n,o){let a=n.timestamps;if(x=a[0],f=a[a.length-1],d){let y=`${a.length}:${x}:${f}`;y!==ie?(ie=y,S=x,A=f):(S=X(S,x,f),A=X(Math.max(A,S+oe()),x,f))}let c=1/0,l=-1/0;for(let y of n.values)te(y)&&(y<c&&(c=y),y>l&&(l=y));for(let y of o)y.scored&&(te(y.lower)&&y.lower<c&&(c=y.lower),te(y.upper)&&y.upper>l&&(l=y.upper));(!te(c)||!te(l))&&(c=0,l=1),l<=c&&(l=c+1);let h=(l-c)*.06;k=c-h,z=l+h}function ae(n,o,a){let c=p.series.timestamps;ze(e,c,n,E(),I(),C.l*i,D(),H,G,o,a,i)}function ge(n,o){let a=[],c=-1;for(let l=Math.max(0,o);l<n.length;l++){let h=n[l];h.scored&&te(h.lower)&&te(h.upper)?c===-1&&(c=l):c!==-1&&(a.push([c,l-1]),c=-1)}return c!==-1&&a.push([c,n.length-1]),a}function Z(){if(m=0,!p||t.width===0||t.height===0)return;let{series:n,scored:o,params:a,alerts:c}=p;if(n.timestamps.length===0){e.fillStyle=O("--term-bg"),e.fillRect(0,0,t.width,t.height);return}let l=O("--clay"),h=O("--st-anomaly"),y=O("--faint"),_=O("--muted");e.fillStyle=O("--term-bg"),e.fillRect(0,0,t.width,t.height),e.font=`${11*i}px ui-monospace, 'JetBrains Mono', monospace`,e.textBaseline="middle";for(let g=0;g<=4;g++){let v=k+(z-k)*g/4,w=G(v);e.strokeStyle=R(y,.1),e.lineWidth=1*i,e.beginPath(),e.moveTo(C.l*i,w),e.lineTo(t.width-C.r*i,w),e.stroke(),e.fillStyle=_,e.textAlign="right",e.fillText(Be(v),(C.l-8)*i,w)}e.textBaseline="top";let F=t.height-C.b*i-N();if(d){let g=Xe(E(),I(),7);for(let v of g.ticks){let w=H(v);e.strokeStyle=R(y,.1),e.lineWidth=1*i,e.beginPath(),e.moveTo(w,C.t*i),e.lineTo(w,F),e.stroke(),e.fillStyle=_,e.textAlign=w<(C.l+24)*i?"left":w>t.width-(C.r+24)*i?"right":"center",e.fillText(Je(v,g.step),w,F+7*i)}}else{let g=P();for(let v=0;v<=5;v++){let w=x+g*v/5,U=H(w);e.fillStyle=_,e.textAlign=v===0?"left":v===5?"right":"center",e.fillText(je(w,g),U,F+7*i)}}e.save(),e.beginPath(),e.rect(C.l*i,C.t*i,D(),T()),e.clip();let B=o.length,M=Math.min(Ve(n,a),B),L=M<B?n.timestamps[M]:void 0,j=ge(o,M);e.fillStyle=R(l,.13);for(let[g,v]of j){e.beginPath(),e.moveTo(H(o[g].timestamp),G(o[g].upper));for(let w=g+1;w<=v;w++)e.lineTo(H(o[w].timestamp),G(o[w].upper));for(let w=v;w>=g;w--)e.lineTo(H(o[w].timestamp),G(o[w].lower));e.closePath(),e.fill()}e.strokeStyle=R(l,.4),e.lineWidth=1*i;for(let[g,v]of j)for(let w of["upper","lower"]){e.beginPath();for(let U=g;U<=v;U++){let Ce=H(o[U].timestamp),Te=G(o[U][w]);U===g?e.moveTo(Ce,Te):e.lineTo(Ce,Te)}e.stroke()}e.strokeStyle=R(y,.55),e.lineWidth=1*i,e.setLineDash([3*i,3*i]);for(let[g,v]of j){e.beginPath();for(let w=g;w<=v;w++){let U=o[w].center;if(!te(U))continue;let Ce=H(o[w].timestamp),Te=G(U);w===g?e.moveTo(Ce,Te):e.lineTo(Ce,Te)}e.stroke()}if(e.setLineDash([]),a.smoothing!=="none"){ae(n.values,R(l,.28),1.25);let g=o.map(v=>v.processedValue);ae(g,l,1.6)}else ae(n.values,l,1.5);for(let g=M;g<B;g++){let v=o[g];if(!v.scored||!te(v.value))continue;let w=H(v.timestamp),U=G(v.value);v.isAnomaly?(e.fillStyle=R(h,.18),e.beginPath(),e.arc(w,U,6*i,0,Math.PI*2),e.fill(),e.fillStyle=h,e.beginPath(),e.arc(w,U,3*i,0,Math.PI*2),e.fill()):n.truthAnomaly[g]&&(e.strokeStyle=R(_,.7),e.lineWidth=1.25*i,e.beginPath(),e.arc(w,U,3.5*i,0,Math.PI*2),e.stroke())}L!==void 0&&Ue(e,t,C,i,H,L,"detection at full power \u2192"),s>=0&&s<o.length&&me(s,a.windowSize,o,n,y),c&&c.length&&Ge(e,t,C,i,H,c,g=>g==="anomaly"?O("--st-anomaly"):g==="recovery"?O("--st-recovery"):O("--st-nodata")),e.restore(),d&&ve(n,c,l,y,_)}function ve(n,o,a,c,l){let h=V(),y=h+K(),_=C.l*i,F=t.width-C.r*i,B=Xe(x,f,5);e.save(),e.beginPath(),e.rect(_,h,ne(),K()),e.clip(),e.strokeStyle=R(c,.1),e.lineWidth=1*i;for(let g of B.ticks){let v=Q(g);e.beginPath(),e.moveTo(v,h),e.lineTo(v,y),e.stroke()}ze(e,n.timestamps,n.values,x,f,_,ne(),Q,Ae,R(a,.7),1.1,i),e.restore();let M=Q(S),L=Q(A);if(e.fillStyle="rgba(27,25,22,0.55)",e.fillRect(_,h,M-_,K()),e.fillRect(L,h,F-L,K()),o&&o.length){let g=O("--st-anomaly");e.fillStyle=R(g,.85);for(let v of o){let w=Q(v.t);e.fillRect(w-1*i,h,2*i,K())}}e.fillStyle="rgba(245,241,232,0.06)",e.fillRect(M,h,L-M,K()),e.strokeStyle=a,e.lineWidth=1.5*i,e.strokeRect(M,h+1,L-M,K()-2),e.fillStyle=a;let j=h+K()/2-8*i;e.fillRect(M-2*i,j,4*i,16*i),e.fillRect(L-2*i,j,4*i,16*i),e.font=`${10*i}px ui-monospace, monospace`,e.textBaseline="top",e.fillStyle=l;for(let g of B.ticks){let v=Q(g);e.strokeStyle=R(c,.25),e.lineWidth=1*i,e.beginPath(),e.moveTo(v,y),e.lineTo(v,y+3*i),e.stroke(),e.textAlign=v<(C.l+26)*i?"left":v>t.width-(C.r+26)*i?"right":"center",e.fillText(Je(g,B.step),v,y+5*i)}}function me(n,o,a,c,l){let h=c.timestamps,y=Math.max(0,n-o),_=n-1,F=C.t*i,B=T();if(_>=y){let j=c.intervalSeconds*1e3/P()*D()*.5,g=H(h[y])-j,v=H(h[_])+j;e.fillStyle="rgba(255,255,255,0.05)",e.fillRect(g,F,v-g,B),e.strokeStyle=R(l,.5),e.lineWidth=1*i,e.beginPath(),e.moveTo(g,F),e.lineTo(g,F+B),e.moveTo(v,F),e.lineTo(v,F+B),e.stroke()}let M=H(h[n]);e.strokeStyle=R(l,.85),e.lineWidth=1*i,e.setLineDash([2*i,2*i]),e.beginPath(),e.moveTo(M,F),e.lineTo(M,F+B),e.stroke(),e.setLineDash([]);let L=a[n];if(L.scored&&te(L.lower)&&te(L.upper)){let j=5*i;e.strokeStyle=R(O("--clay"),.85),e.lineWidth=1.5*i;for(let g of[L.lower,L.upper]){let v=G(g);e.beginPath(),e.moveTo(M-j,v),e.lineTo(M+j,v),e.stroke()}}if(te(L.value)){let j=G(L.value);e.fillStyle=O("--term-bg"),e.beginPath(),e.arc(M,j,4*i,0,Math.PI*2),e.fill(),e.strokeStyle=L.isAnomaly?O("--st-anomaly"):O("--clay"),e.lineWidth=2*i,e.beginPath(),e.arc(M,j,4*i,0,Math.PI*2),e.stroke()}}function re(){m===0&&(m=requestAnimationFrame(Z))}function fe(){var a;if(!r.onHover||!p)return;if(s<0){r.onHover(null);return}let n=(a=p.scored[s])!=null?a:null,o={index:s,point:n,windowStart:Math.max(0,s-p.params.windowSize),windowEnd:s-1};r.onHover(o)}let ee=null,$=null,le=n=>{let o=t.getBoundingClientRect();return{x:(n.clientX-o.left)*i,y:(n.clientY-o.top)*i}},xe=n=>d&&n>=V(),ye=n=>{let o=Q(S),a=Q(A),c=8*i;return Math.abs(n-o)<=c?"l":Math.abs(n-a)<=c?"r":n>o&&n<a?"move":"out"};function pe(n){if(!p)return;let{x:o,y:a}=le(n);if(ee||$)return;if(xe(a)){s!==-1&&(s=-1,fe(),re());let l=ye(o);t.style.cursor=l==="l"||l==="r"?"ew-resize":l==="move"?"grab":"pointer";return}d&&(t.style.cursor="grab");let c=Pe(o);c!==s&&(s=c,fe(),re())}function we(){s!==-1&&(s=-1,fe(),re())}function ke(n){if(!d||!p)return;let{x:o,y:a}=le(n);if(a>=V()){let c=ye(o);if(c==="l")$={type:"l",grab:0,vMin:S,vMax:A};else if(c==="r")$={type:"r",grab:0,vMin:S,vMax:A};else if(c==="move")$={type:"move",grab:se(o),vMin:S,vMax:A};else{let l=se(o),h=P();J(l-h/2,l+h/2),$={type:"move",grab:l,vMin:S,vMax:A}}t.style.cursor="grabbing",n.preventDefault()}else a>C.t*i&&a<V()-Ke*i&&(ee={x:o,vMin:S,vMax:A},t.style.cursor="grabbing",n.preventDefault())}function Ee(n){if(!p)return;let{x:o}=le(n);if(ee){let a=(o-ee.x)*(ee.vMax-ee.vMin)/(D()||1);J(ee.vMin-a,ee.vMax-a)}else if($){let a=se(o);$.type==="l"?J(Math.min(a,A-oe()),A):$.type==="r"?J(S,Math.max(a,S+oe())):J($.vMin+(a-$.grab),$.vMax+(a-$.grab))}}function he(){(ee||$)&&(ee=null,$=null,t.style.cursor="grab")}function be(n){if(!d||!p)return;n.preventDefault();let{x:o,y:a}=le(n),c=X(P()*Math.pow(1.0015,n.deltaY),oe(),Y());if(a>=V()){let l=se(o);J(l-c/2,l+c/2)}else{let l=De(o),h=(l-S)/(P()||1);J(l-h*c,l-h*c+c)}}function Se(){d&&J(x,f)}t.addEventListener("mousemove",pe),t.addEventListener("mouseleave",we),d&&(t.addEventListener("mousedown",ke),t.addEventListener("wheel",be,{passive:!1}),t.addEventListener("dblclick",Se),window.addEventListener("mousemove",Ee),window.addEventListener("mouseup",he));function Me(n){p=n,Le(n.series,n.scored),s>=n.series.timestamps.length&&(s=-1),re()}function Ne(){W(),p&&(Le(p.series,p.scored),re())}function _e(){t.removeEventListener("mousemove",pe),t.removeEventListener("mouseleave",we),d&&(t.removeEventListener("mousedown",ke),t.removeEventListener("wheel",be),t.removeEventListener("dblclick",Se),window.removeEventListener("mousemove",Ee),window.removeEventListener("mouseup",he)),m!==0&&cancelAnimationFrame(m),m=0,p=null}return W(),{render:Me,resize:Ne,destroy:_e}}var st={mad:3,zscore:3,iqr:1.5},at={mad:10,zscore:3,iqr:4},lt="dtk-tune";function b(t,r,u){let e=document.createElement(t);return r&&(e.className=r),u!=null&&(e.textContent=u),e}function He(t,r){let u=b("label","dtk-ctl-label",t);if(r){u.title=r;let e=b("span","dtk-ctl-info","\u24D8");e.title=r,u.appendChild(document.createTextNode(" ")),u.appendChild(e)}return u}function We(t,r,u,e,d){let i=b("div","dtk-ctl");i.appendChild(He(t,d));let p=b("div","dtk-seg"),s=u,m=[],x=()=>{m.forEach(f=>f.classList.toggle("on",f.dataset.v===s))};return r.forEach(f=>{let k=b("button","dtk-seg-btn",f.label);k.type="button",k.dataset.v=f.value,k.onclick=()=>{s=f.value,x(),e(s)},m.push(k),p.appendChild(k)}),x(),i.appendChild(p),{row:i,get:()=>s,set:f=>{s=f,x()}}}function Re(t,r,u){var x;let e=b("div","dtk-ctl"),d=b("div","dtk-ctl-head"),i=He(t,r.hint),p=b("span","dtk-ctl-val"),s=(x=r.fmt)!=null?x:(f=>String(f));d.appendChild(i),d.appendChild(p),e.appendChild(d);let m=b("input","dtk-range");return m.type="range",m.min=String(r.min),m.max=String(r.max),m.step=String(r.step),m.value=String(r.value),p.textContent=s(r.value),m.oninput=()=>{let f=Number(m.value);p.textContent=s(f),u(f)},e.appendChild(m),{row:e,get:()=>Number(m.value),setMax:f=>{m.max=String(f),Number(m.value)>f&&(m.value=String(f),p.textContent=s(f))}}}function et(t){let r={threshold:t.threshold,window_size:t.windowSize};return t.windowWeights!=="none"&&(r.window_weights=t.windowWeights,t.windowWeights==="exponential"&&t.halfLife!=null&&(r.half_life=t.halfLife)),t.detrend!=="none"&&(r.detrend=t.detrend),t.smoothing!=="none"&&(r.smoothing=t.smoothing),t.inputType!=="values"&&(r.input_type=t.inputType),t.seasonalityComponents&&t.seasonalityComponents.length&&(r.seasonality_components=t.seasonalityComponents,r.min_samples_per_group=t.minSamplesPerGroup),r}function ct(t,r){let u=et(t),e=[`type: ${t.type}`];for(let[d,i]of Object.entries(u))e.push(`${d}=${typeof i=="object"?JSON.stringify(i):i}`);return e.push(`consecutive_anomalies=${r}`),e.join(" \xB7 ")}function ut(t,r){var Ne,_e;pt(),r.classList.add(lt),r.innerHTML="";let u=b("div","dtk-tune-root");r.appendChild(u);let e=t.points.length,d={timestamps:t.points.map(n=>n.t),values:t.points.map(n=>n.v==null?NaN:n.v),intervalSeconds:t.interval_seconds,truthAnomaly:new Array(e).fill(!1),seasonalityData:t.seasonality_columns.length?t.seasonality:void 0,seasonalityColumns:t.seasonality_columns.length?t.seasonality_columns:void 0},i=d,p=n=>{let o=Math.max(0,e-n);return o<=0?d:{timestamps:d.timestamps.slice(o),values:d.values.slice(o),intervalSeconds:d.intervalSeconds,truthAnomaly:d.truthAnomaly.slice(o),seasonalityData:d.seasonalityData?d.seasonalityData.slice(o):void 0,seasonalityColumns:d.seasonalityColumns}},s=t.detector,m=t.consecutive_anomalies,x=(Ne=s.seasonalityComponents)!=null?Ne:[],f=new Map;x.forEach((n,o)=>n.forEach(a=>f.set(a,o+1)));let k=()=>{let n=0;f.forEach(a=>{a>n&&(n=a)});let o=[];for(let a=1;a<=n;a++){let c=t.seasonality_columns.filter(l=>f.get(l)===a);c.length&&o.push(c)}return o.length?o:null},z=()=>{var n;return{type:ve.get(),threshold:me.get(),windowSize:le.get(),minSamples:s.minSamples,inputType:s.inputType,smoothing:ke.get(),smoothingAlpha:s.smoothingAlpha,smoothingWindow:s.smoothingWindow,windowWeights:xe.get(),halfLife:xe.get()==="exponential"?ye.get():null,detrend:we.get(),seasonalityComponents:k(),minSamplesPerGroup:(n=at[ve.get()])!=null?n:s.minSamplesPerGroup,consecutiveAnomalies:m}},S=b("div","dtk-tune-header"),A=b("div","dtk-tune-titlerow");A.appendChild(b("h1","dtk-tune-title",t.metric));let ie=b("span","dtk-tune-badge",t.save_url?"manual tuning":"preview");A.appendChild(ie),S.appendChild(A);let W=t.project?`${t.project} \xB7 `:"";S.appendChild(b("div","dtk-tune-sub",`${W}${e} points \xB7 ${ft(t.interval_seconds)} grid`)),t.description&&S.appendChild(b("div","dtk-tune-desc",t.description)),u.appendChild(S);let N=b("div","dtk-tune-grid"),D=b("div","dtk-tune-controls"),T=b("div","dtk-tune-main");N.appendChild(D),N.appendChild(T),u.appendChild(N);let E=b("div","dtk-tune-trim"),I=b("div","dtk-tune-trim-head");I.appendChild(He("Points shown","Trim the active sample to the most-recent N points. Fewer points recompute faster (cost grows with points \xD7 window) and make a shorter period easier to read \u2014 handy once you can see a smaller window/period is enough."));let P=b("span","dtk-tune-trim-val");I.appendChild(P),E.appendChild(I);let Y=b("input","dtk-range");Y.type="range",Y.min=String(Math.min(e,200)),Y.max=String(e),Y.step=String(Math.max(1,Math.round(e/200))),Y.value=String(e),E.appendChild(Y),T.appendChild(E);let H=b("div","dtk-tune-chart"),G=b("canvas");H.appendChild(G);let V=b("div","dtk-tune-spin");V.appendChild(b("span","dtk-spin-ring")),V.appendChild(b("span","dtk-spin-txt","computing\u2026")),H.appendChild(V),T.appendChild(H);let K=b("div","dtk-tune-legend"),ne=(n,o,a)=>{let c=b("span","dtk-leg-item");c.title=a,c.appendChild(b("span",`dtk-leg-sw ${n}`)),c.appendChild(b("span","dtk-leg-txt",o)),K.appendChild(c)};ne("line","metric","The metric value over time."),ne("band","expected range","The detector's confidence band \u2014 values inside it read as normal."),ne("center","band center","The expected value at the middle of the band."),ne("dot","anomaly","A point the detector flagged as anomalous (outside the band)."),ne("alert","alert","Where an alert fired \u2014 enough consecutive anomalies to meet the rule."),T.appendChild(K);let Q=b("div","dtk-tune-readout");T.appendChild(Q);let Ae=Qe(G,{navigable:!0,onHover:n=>{if(!n||!n.point||!n.point.scored){Q.textContent="";return}let o=n.point;Q.textContent=`t=${dt(o.timestamp)} value=${qe(o.value)} band=[${qe(o.lower)}, ${qe(o.upper)}]`+(o.isAnomaly?` \u26A0 ${o.direction} (sev ${o.severity.toFixed(2)})`:"")}}),X=new Worker(URL.createObjectURL(new Blob([`"use strict";(()=>{var X={mad:1,zscore:2,iqr:4};function T(n){let e=0;for(let t of n)e+=t;return e}function ie(n){let e=n.map((t,r)=>r);return e.sort((t,r)=>n[t]<n[r]?-1:n[t]>n[r]?1:t-r),e}function se(n,e,t){let r=e.length;if(r===0)return NaN;if(n<=e[0])return t[0];if(n>=e[r-1])return t[r-1];for(let i=1;i<r;i++)if(n<=e[i]){let o=e[i-1],c=e[i],u=t[i-1],s=t[i];if(c===o)return u;let d=(n-o)/(c-o);return u+d*(s-u)}return t[r-1]}function F(n,e,t){let r=ie(n),i=r.map(b=>n[b]),o=r.map(b=>e[b]),c=T(o),u=new Array(o.length),s=0;for(let b=0;b<o.length;b++)s+=o[b],u[b]=(s-.5*o[b])/c;let d=t/100;return se(d,u,i)}function S(n,e){return F(n,e,50)}function ce(n,e,t){let r=t===void 0?S(n,e):t,i=n.map(o=>Math.abs(o-r));return S(i,e)}function ue(n,e){let t=T(e),r=0;for(let i=0;i<n.length;i++)r+=n[i]*(e[i]/t);return r}function me(n,e,t,r=0){let i=T(e),o=e.map(s=>s/i),c=t===void 0?T(n.map((s,d)=>s*o[d])):t,u=0;for(let s=0;s<n.length;s++)u+=o[s]*(n[s]-c)**2;if(r===1){let s=1-T(o.map(d=>d*d));s>1e-12&&(u/=s)}return Math.sqrt(u)}function le(n,e){return e.smoothing==="none"?n.slice():e.smoothing==="ema"?ae(n,e.smoothingAlpha):fe(n,e.smoothingWindow)}function ae(n,e){let t=n.length,r=new Array(t).fill(NaN);if(t===0)return r;let i=-1;for(let o=0;o<t;o++)if(!Number.isNaN(n[o])){i=o;break}if(i===-1)return r;r[i]=n[i];for(let o=i+1;o<t;o++)Number.isNaN(n[o])?r[o]=r[o-1]:r[o]=e*n[o]+(1-e)*r[o-1];return r}function fe(n,e){let t=n.length,r=new Array(t).fill(NaN);for(let i=0;i<t;i++){let o=Math.max(0,i-e+1),c=0,u=0;for(let s=o;s<=i;s++)Number.isNaN(n[s])||(c+=n[s],u+=1);r[i]=u>0?c/u:NaN}return r}function de(n,e){let t=n.length;if(e.inputType==="values")return n.slice();let r=new Array(t).fill(NaN);for(let i=1;i<t;i++){let o=n[i-1],c=n[i];e.inputType==="changes"?r[i]=(c-o)/o:e.inputType==="absolute_changes"?r[i]=c-o:r[i]=Math.log(c+1)-Math.log(o+1)}return r}function be(n,e){return n.halfLife!==null?n.halfLife:Math.max(n.windowSize/20,e/2,1)}function pe(n,e){if(n.windowWeights==="none")return null;let t=n.windowSize,r=new Array(t);if(n.windowWeights==="exponential"){let i=be(n,e);for(let o=1;o<=t;o++){let c=Math.min(o/i,1e3);r[o-1]=Math.pow(.5,c)}return r}for(let i=1;i<=t;i++)r[i-1]=(t+1-i)/t;return r}function K(n,e){return e===null?n.map(()=>1):n.map(t=>e[t-1])}function ge(n){let e=n[0];for(let t of n)t>e&&(e=t);return e}function ye(n){let e=n[0];for(let t of n)t<e&&(e=t);return e}function he(n,e,t){if(n.length<4)return 0;let r=(ge(e)+ye(e))/2,i=e.map(l=>l>r),o=i.map(l=>!l),c=0,u=0;for(let l=0;l<e.length;l++)i[l]?c++:u++;if(c<2||u<2)return 0;let s=(l,N)=>N.filter((L,y)=>l[y]),d=s(o,n),b=s(i,n),k=s(o,e),C=s(i,e),g=s(o,t),f=s(i,t),q=S(d,g),p=S(b,f),w=S(k,g),M=S(C,f);return M===w?0:(q-p)/(M-w)}var Se={mad:[["median","center"],["mad","spread"]],zscore:[["mean","center"],["std","spread"]],iqr:[["q1","center"],["q3","center"],["iqr","spread"]]};function Q(n,e,t){if(n==="mad"){let o=S(e,t);return{median:o,mad:ce(e,t,o)}}if(n==="zscore"){let o=ue(e,t);return{mean:o,std:me(e,t,o,1)}}let r=F(e,t,25),i=F(e,t,75);return{q1:r,q3:i,iqr:i-r}}function we(n,e,t){if(n==="mad"){if(e.mad===0)return[e.median-1e-10,e.median+1e-10];let r=t*1.4826*e.mad;return[e.median-r,e.median+r]}return n==="zscore"?e.std===0?[e.mean-1e-10,e.mean+1e-10]:[e.mean-t*e.std,e.mean+t*e.std]:e.iqr===0?[e.q1-1e-10,e.q3+1e-10]:[e.q1-t*e.iqr,e.q3+t*e.iqr]}function Me(n,e,t){if(n==="mad"){let r=1.4826*e.mad;return r>0?t/r:1/0}return n==="zscore"?e.std>0?t/e.std:1/0:e.iqr>0?t/e.iqr:1/0}function Ne(n,e){return n==="mad"?e.median:n==="zscore"?e.mean:(e.q1+e.q3)/2}function U(n,e,t,r,i){return{index:n,timestamp:e,value:t,processedValue:r,scored:!1,isAnomaly:!1,lower:NaN,upper:NaN,center:NaN,direction:null,severity:0,reason:i}}function Y(n,e){let{type:t}=e,r=Math.max(e.minSamples,X[t]),i=Se[t],{timestamps:o,values:c}=n,u=o.length,s=le(c,e),d=de(s,e),b=e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0,k=n.seasonalityData,C=pe(e,r),g=[];for(let f=0;f<u;f++){let q=c[f],p=d[f],w=o[f];if(Number.isNaN(p)){g.push(U(f,w,q,p,"missing_data"));continue}let M=Math.max(0,f-e.windowSize),l=f-M,N=d.slice(M,f),L=N.map(m=>!Number.isNaN(m)),y=[];for(let m=0;m<N.length;m++)L[m]&&y.push(N[m]);if(y.length<r){g.push(U(f,w,q,p,"insufficient_data"));continue}let O=new Array(l);for(let m=0;m<l;m++)O[m]=l-m;let z=[];for(let m=0;m<l;m++)L[m]&&z.push(O[m]);let V=K(z,C),P=0;e.detrend==="linear"&&(P=he(y,z,V));let $=P!==0?y.map((m,W)=>m+P*z[W]):y,G=Q(t,$,V),I={...G};if(b&&k){let m=k[f];for(let W of e.seasonalityComponents){let J=new Array(l).fill(!0);for(let a=0;a<l;a++){let D=k[M+a],x=!0;for(let j of W)if(!D||D[j]!==(m==null?void 0:m[j])){x=!1;break}J[a]=x}let _=[],E=[];for(let a=0;a<l;a++)L[a]&&J[a]&&(_.push(N[a]),E.push(O[a]));if(_.length<e.minSamplesPerGroup)continue;let ne=K(E,C),te=P!==0?_.map((a,D)=>a+P*E[D]):_,re=Q(t,te,ne);for(let[a,D]of i){let x=G[a],oe=(D==="spread"?x>0:x!==0)?re[a]/x:1;I[a]*=oe}}}let[h,A]=we(t,I,e.threshold);h>A&&([h,A]=[A,h]);let H=p<h||p>A,R=null,B=0;if(H){let m;p<h?(R="below",m=h-p):(R="above",m=p-A),B=Me(t,I,m)}let ee=Ne(t,I);g.push({index:f,timestamp:w,value:q,processedValue:p,scored:!0,isAnomaly:H,lower:h,upper:A,center:ee,direction:R,severity:B,reason:"ok"})}return g}function Ae(n,e){let t=n.seasonalityData;if(!t||t.length===0)return 0;let r=0;for(let i of e){let o=new Set;for(let c of t)o.add(i.map(u=>{var s;return String((s=c==null?void 0:c[u])!=null?s:"")}).join("|"));r=Math.max(r,o.size)}return r}function Z(n,e){let t=n.timestamps.length,r=Math.max(e.minSamples,X[e.type]);if(e.smoothing==="sma"&&(r=Math.max(r,e.smoothingWindow-1)),e.smoothing==="ema"&&(r=Math.max(r,Math.ceil(5/e.smoothingAlpha))),e.inputType!=="values"&&(r=Math.max(r,1)),e.seasonalityComponents!==null&&e.seasonalityComponents.length>0&&Array.isArray(n.seasonalityData)&&n.seasonalityData.length>0){let o=Ae(n,e.seasonalityComponents);if(o>0){let c=e.minSamplesPerGroup*o;e.windowSize>=c&&(r=Math.max(r,c))}}return Math.min(r,t)}var v=null;function De(n,e,t){let r=[],i=0;for(let o=0;o<n.length;o++){if(!(n[o].scored&&n[o].isAnomaly)){i=0;continue}let u=o>0&&n[o-1].scored&&n[o-1].isAnomaly,s=o>0&&n[o].timestamp-n[o-1].timestamp===e;i=u&&s?i+1:1,i===t&&r.push(o)}return r}self.onmessage=n=>{let e=n.data;if(e.type==="series"){v=e.series;return}if(e.type==="run"&&v){let t=e.params,r=Y(v,t),i=v.intervalSeconds*1e3,o=De(r,i,t.consecutiveAnomalies),c=Z(v,t),u=0;for(let s of r)s.scored&&s.isAnomaly&&u++;self.postMessage({type:"result",id:e.id,scored:r,fires:o,eff:c,flagged:u})}};})();
2
- `],{type:"text/javascript"})));X.postMessage({type:"series",series:i});let oe=0,J=null;X.onmessage=n=>{let o=n.data;if(o.type!=="result"||o.id!==oe||!J)return;V.classList.remove("on");let a=J,c=o.fires.map(l=>({t:i.timestamps[l],kind:"anomaly"}));Ae.render({series:i,scored:o.scored,params:a,alerts:c}),he.textContent=`${o.flagged} flagged \xB7 ${o.fires.length} alert${o.fires.length===1?"":"s"} \xB7 warm-up ${o.eff} pts`,Se.textContent=ct(a,m)},X.onerror=()=>{V.classList.remove("on"),he.textContent="recompute failed \u2014 see the browser console"};let De=()=>{J=z(),oe+=1,V.classList.add("on"),X.postMessage({type:"run",id:oe,params:J})},se=n=>n>1?mt(d.timestamps[e-1]-d.timestamps[e-n]):"\u2014",Pe=n=>{P.textContent=n>=e?`${e} pts \xB7 full (${se(e)})`:`${n} pts \xB7 ${se(n)}`};Pe(e);function Le(n){i=p(n),X.postMessage({type:"series",series:i}),Z()}let ae=0;Y.oninput=()=>{let n=Number(Y.value);Pe(n),ae&&window.clearTimeout(ae),ae=window.setTimeout(()=>Le(n),200)};let ge=0,Z=()=>{ge&&window.clearTimeout(ge),ge=window.setTimeout(De,130)},ve=We("Detector",[{label:"MAD",value:"mad"},{label:"Z-Score",value:"zscore"},{label:"IQR",value:"iqr"}],s.type,n=>{var o;ee((o=st[n])!=null?o:3),Z()},"The statistic used for the center/spread of the band: MAD (robust median, default), Z-Score (mean/std) or IQR (quartiles). All share the same windowing.");D.appendChild(ve.row);let me=Re("Threshold (\u03C3-equivalent)",{min:.5,max:10,step:.1,value:s.threshold,fmt:n=>n.toFixed(1),hint:"Band half-width in \u03C3-equivalents. Lower = tighter band = more flags; higher = wider band = fewer flags."},Z);D.appendChild(me.row);let re=me.row.querySelector("input"),fe=me.row.querySelector(".dtk-ctl-val"),ee=n=>{re&&(re.value=String(n)),fe&&(fe.textContent=n.toFixed(1))},$=Math.max(50,Math.min(2e3,Math.floor(e/2))),le=Re("Window size (points)",{min:10,max:$,step:5,value:Math.min(s.windowSize,$),fmt:n=>String(n),hint:"How many trailing points form the baseline window for each scored point. Larger = steadier baseline (more history); smaller = adapts faster to shifts."},Z);D.appendChild(le.row);let xe=We("Recency weighting",[{label:"none",value:"none"},{label:"exponential",value:"exponential"},{label:"linear",value:"linear"}],s.windowWeights,n=>{pe.style.display=n==="exponential"?"":"none",Z()},"Weight recent points in the window more heavily: none (flat), exponential (half-life decay) or linear. Helps the baseline track a drifting level.");D.appendChild(xe.row);let ye=Re("Half-life (points)",{min:1,max:$,step:1,value:(_e=s.halfLife)!=null?_e:Math.max(5,Math.round(s.windowSize/20)),fmt:n=>String(n),hint:"Exponential weighting only: the age (in points) at which a point counts half as much as the newest. Smaller = faster decay = fresher baseline."},Z),pe=ye.row;pe.style.display=s.windowWeights==="exponential"?"":"none",D.appendChild(pe);let we=We("Detrend",[{label:"none",value:"none"},{label:"linear",value:"linear"}],s.detrend,Z,"Remove a robust linear trend from each window before computing the band, so a steadily rising/falling metric is not flagged for the trend itself.");D.appendChild(we.row);let ke=We("Smoothing",[{label:"none",value:"none"},{label:"EMA",value:"ema"},{label:"SMA",value:"sma"}],s.smoothing,Z,"Smooth the series before detection (EMA or SMA) so single-point jitter does not flag. The detector judges the smoothed line; the raw values show as a faint ghost.");if(D.appendChild(ke.row),t.seasonality_columns.length){let n=t.seasonality_columns,o=b("div","dtk-ctl");o.appendChild(He("Seasonality groups","Condition the band on seasonal keys. Pick a group per column: columns in the SAME group are combined into one key (e.g. dow\xD7hour); separate groups each apply their own correction. Off = ignore that column."));let a=Math.min(n.length,6),c=[{label:"\u2014",value:"0"}];for(let l=1;l<=a;l++)c.push({label:`G${l}`,value:String(l)});n.forEach(l=>{let h=b("div","dtk-season-row");h.appendChild(b("span","dtk-season-col",l));let y=b("div","dtk-seg dtk-season-seg"),_=[],F=()=>{var M;return(M=f.get(l))!=null?M:0},B=()=>_.forEach(M=>M.classList.toggle("on",Number(M.dataset.v)===F()));c.forEach(M=>{let L=b("button","dtk-seg-btn",M.label);L.type="button",L.dataset.v=M.value,L.title=M.value==="0"?`ignore ${l}`:`put ${l} in group ${M.value}`,L.onclick=()=>{f.set(l,Number(M.value)),B(),Z()},_.push(L),y.appendChild(L)}),B(),h.appendChild(y),o.appendChild(h)}),D.appendChild(o)}let Ee=Re("Alert: consecutive anomalies",{min:1,max:10,step:1,value:m,fmt:n=>String(n),hint:"How many anomalies in a row are required before an alert fires. Higher = fewer, more-confident alerts (the \u25BC markers on the chart)."},n=>{m=n,Z()});D.appendChild(Ee.row);let he=b("div","dtk-tune-stat");T.appendChild(he);let be=b("div","dtk-tune-cfg");be.appendChild(b("span","dtk-tune-cfg-k","// effective config"));let Se=b("code","dtk-tune-cfg-v");if(be.appendChild(Se),T.appendChild(be),t.save_url){let n=b("div","dtk-tune-apply"),o=b("button","dtk-apply-btn","Apply to metric");o.type="button",o.title="Write this detector config back into the metric YAML (the previous version is archived under metrics/.history/). Trimming the sample does not change what is written.";let a=b("span","dtk-apply-msg");o.onclick=()=>{let c=z();o.disabled=!0,a.className="dtk-apply-msg info",a.textContent="Applying\u2026",fetch(t.save_url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({detector:{type:c.type,params:et(c)},consecutive_anomalies:m})}).then(l=>l.ok?l.json():l.text().then(h=>{throw new Error(h||`HTTP ${l.status}`)})).then(l=>{var h;a.className="dtk-apply-msg ok",a.textContent=`Applied \u2192 ${(h=l.saved)!=null?h:"metric"} (previous archived). You can close this tab.`}).catch(l=>{o.disabled=!1,a.className="dtk-apply-msg err",a.textContent=`Apply failed: ${l.message}`})},n.appendChild(o),n.appendChild(a),T.appendChild(n)}else T.appendChild(b("div","dtk-tune-note","Static preview \u2014 sliders recompute live, but there is no write-back. Run `dtk tune` (without --no-serve) to apply a config."));De();let Me=0;window.addEventListener("resize",()=>{Me&&cancelAnimationFrame(Me),Me=requestAnimationFrame(()=>Ae.resize())})}function qe(t){if(!Number.isFinite(t))return"\u2014";let r=Math.abs(t);return r!==0&&(r<.01||r>=1e6)?t.toExponential(2):Number(t.toFixed(r<1?4:2)).toString()}function dt(t){return new Date(t).toISOString().slice(0,16).replace("T"," ")}function mt(t){let r=Math.round(t/6e4);if(r<60)return`${r}m`;let u=Math.floor(r/60);if(u<24)return`${u}h`;let e=Math.floor(u/24),d=u%24;return d?`${e}d ${d}h`:`${e}d`}function ft(t){return t%86400===0?`${t/86400}d`:t%3600===0?`${t/3600}h`:t%60===0?`${t/60}min`:`${t}s`}var Ze=!1;function pt(){if(Ze)return;Ze=!0;let t=`
3
- .dtk-tune{--c:#d15b36;--c7:#b4471f;--ink:#1b1916;--muted:#6e675b;--faint:#9a9384;
4
- --paper:#f5f1e8;--surface:#fbf9f3;--border:#e6e0d4;--green:#2e9e73;--anom:#d63232;
5
- --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
6
- --sans:'Schibsted Grotesk',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
7
- .dtk-tune-root{max-width:1200px;margin:0 auto;padding:24px 20px 56px;font-family:var(--sans);color:var(--ink);}
8
- .dtk-tune-titlerow{display:flex;align-items:center;gap:12px;}
9
- .dtk-tune-title{font-size:24px;margin:0;font-weight:700;}
10
- .dtk-tune-badge{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
11
- color:#fff;background:var(--c);border-radius:999px;padding:3px 10px;}
12
- .dtk-tune-sub{color:var(--muted);font-size:13px;margin-top:4px;font-family:var(--mono);}
13
- .dtk-tune-desc{color:var(--muted);font-size:13px;margin-top:8px;white-space:pre-wrap;}
14
- .dtk-tune-grid{display:grid;grid-template-columns:280px 1fr;gap:24px;margin-top:20px;align-items:start;}
15
- @media(max-width:820px){.dtk-tune-grid{grid-template-columns:1fr;}}
16
- .dtk-tune-controls{display:flex;flex-direction:column;gap:16px;background:var(--surface);
17
- border:1px solid var(--border);border-radius:12px;padding:16px;}
18
- .dtk-ctl{display:flex;flex-direction:column;gap:6px;}
19
- .dtk-ctl-head{display:flex;justify-content:space-between;align-items:baseline;}
20
- .dtk-ctl-label{font-size:12px;font-weight:600;color:var(--ink);}
21
- .dtk-ctl-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
22
- .dtk-seg{display:flex;gap:4px;background:var(--paper);border:1px solid var(--border);border-radius:8px;padding:3px;}
23
- .dtk-seg.dtk-wrap{flex-wrap:wrap;}
24
- .dtk-seg-btn{flex:1 1 auto;border:0;background:transparent;color:var(--muted);font-family:var(--sans);
25
- font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;white-space:nowrap;}
26
- .dtk-seg-btn:hover{color:var(--ink);}
27
- .dtk-seg-btn.on{background:var(--c);color:#fff;font-weight:600;}
28
- .dtk-range{width:100%;accent-color:var(--c);cursor:pointer;}
29
- .dtk-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:2px;cursor:pointer;}
30
- .dtk-tune-main{display:flex;flex-direction:column;gap:10px;min-width:0;}
31
- .dtk-tune-chart{position:relative;width:100%;height:470px;background:var(--surface);
32
- border:1px solid var(--border);border-radius:12px;overflow:hidden;}
33
- .dtk-tune-chart canvas{width:100%;height:100%;display:block;}
34
- .dtk-tune-readout{font-family:var(--mono);font-size:12px;color:var(--muted);min-height:18px;}
35
- .dtk-tune-stat{font-family:var(--mono);font-size:12px;color:var(--ink);}
36
- .dtk-tune-cfg{background:var(--ink);color:#c9c2b4;border-radius:8px;padding:10px 12px;font-family:var(--mono);
37
- font-size:12px;overflow-x:auto;}
38
- .dtk-tune-cfg-k{color:var(--faint);display:block;margin-bottom:4px;}
39
- .dtk-tune-cfg-v{color:#e6e0d4;white-space:pre-wrap;word-break:break-word;}
40
- .dtk-tune-apply{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
41
- .dtk-apply-btn{background:var(--c);color:#fff;border:0;border-radius:8px;padding:10px 18px;font-family:var(--sans);
42
- font-size:14px;font-weight:600;cursor:pointer;}
43
- .dtk-apply-btn:hover{background:var(--c7);}
44
- .dtk-apply-btn:disabled{opacity:.55;cursor:default;}
45
- .dtk-apply-msg{font-size:13px;}
46
- .dtk-apply-msg.ok{color:var(--green);}
47
- .dtk-apply-msg.err{color:var(--anom);}
48
- .dtk-apply-msg.info{color:var(--muted);}
49
- .dtk-tune-note{font-size:13px;color:var(--muted);background:var(--surface);border:1px dashed var(--border);
50
- border-radius:8px;padding:10px 12px;}
51
- .dtk-ctl-info{color:var(--faint);font-size:10px;cursor:help;vertical-align:super;}
52
- .dtk-tune-trim{display:flex;flex-direction:column;gap:6px;background:var(--surface);
53
- border:1px solid var(--border);border-radius:10px;padding:9px 12px;}
54
- .dtk-tune-trim-head{display:flex;justify-content:space-between;align-items:baseline;}
55
- .dtk-tune-trim-val{font-family:var(--mono);font-size:12px;color:var(--c7);}
56
- .dtk-tune-spin{position:absolute;top:10px;right:12px;display:none;align-items:center;gap:7px;
57
- background:rgba(27,25,22,0.78);color:#e6e0d4;border:1px solid #332f29;border-radius:999px;
58
- padding:4px 11px 4px 8px;font-family:var(--mono);font-size:11px;pointer-events:none;}
59
- .dtk-tune-spin.on{display:inline-flex;}
60
- .dtk-spin-ring{width:12px;height:12px;border-radius:50%;border:2px solid rgba(245,241,232,0.25);
61
- border-top-color:var(--c);animation:dtk-spin .7s linear infinite;}
62
- @keyframes dtk-spin{to{transform:rotate(360deg);}}
63
- .dtk-tune-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:2px 2px 0;}
64
- .dtk-leg-item{display:inline-flex;align-items:center;gap:6px;cursor:help;}
65
- .dtk-leg-sw{display:inline-block;flex:0 0 auto;}
66
- .dtk-leg-sw.line{width:16px;height:3px;background:var(--c);border-radius:2px;}
67
- .dtk-leg-sw.band{width:16px;height:11px;background:rgba(209,91,54,0.18);
68
- border:1px solid rgba(209,91,54,0.5);border-radius:2px;}
69
- .dtk-leg-sw.center{width:16px;height:2px;
70
- background:repeating-linear-gradient(90deg,var(--faint) 0 4px,transparent 4px 7px);}
71
- .dtk-leg-sw.dot{width:9px;height:9px;border-radius:50%;background:var(--anom);}
72
- .dtk-leg-sw.alert{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
73
- border-top:7px solid var(--anom);}
74
- .dtk-leg-txt{white-space:nowrap;}
75
- .dtk-season-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
76
- .dtk-season-col{font-family:var(--mono);font-size:11.5px;color:var(--muted);
77
- overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
78
- .dtk-season-seg{flex:0 0 auto;padding:2px;}
79
- .dtk-season-seg .dtk-seg-btn{flex:0 0 auto;padding:3px 7px;font-family:var(--mono);font-size:11px;}
80
- `,r=document.createElement("style");r.textContent=t,document.head.appendChild(r)}window.__DTK_TUNE__={render:ut};})();
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes