nucleus-mcp 1.0.7 → 1.2.0

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 (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. package/tests/test_search.py +0 -69
@@ -1,1087 +0,0 @@
1
- """
2
- DashboardEngine - Enterprise-Grade Orchestration Dashboard
3
- Real-time visibility into all NOP V3.1 components.
4
-
5
- Features:
6
- - 6 metric categories, 25+ metrics
7
- - ASCII, JSON, Mermaid output formats
8
- - Alert engine with configurable thresholds
9
- - Trend analysis with 7-day JSONL persistence
10
- - Snapshot creation and comparison
11
- - <100ms render time for 10K tasks
12
- - Graceful degradation on component failure
13
-
14
- Scales: 10K tasks, 100 agents, 100 concurrent viewers
15
-
16
- Author: NOP V3.1 - January 2026
17
- """
18
-
19
- import json
20
- import time
21
- import threading
22
- import hashlib
23
- from pathlib import Path
24
- from typing import Dict, List, Optional, Any, Tuple
25
- from dataclasses import dataclass, field
26
- from enum import Enum
27
- from datetime import datetime, timedelta
28
-
29
-
30
- class AlertLevel(str, Enum):
31
- """Alert severity levels."""
32
- CRITICAL = "critical"
33
- WARNING = "warning"
34
- INFO = "info"
35
-
36
-
37
- class MetricCategory(str, Enum):
38
- """Metric category types."""
39
- AGENTS = "agents"
40
- TASKS = "tasks"
41
- INGESTION = "ingestion"
42
- COST = "cost"
43
- DEPS = "deps"
44
- SYSTEM = "system"
45
-
46
-
47
- class OutputFormat(str, Enum):
48
- """Supported output formats."""
49
- ASCII = "ascii"
50
- JSON = "json"
51
- MERMAID = "mermaid"
52
-
53
-
54
- @dataclass
55
- class Alert:
56
- """Alert data structure."""
57
- level: AlertLevel
58
- metric: str
59
- message: str
60
- value: float
61
- threshold: float
62
- timestamp: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
63
-
64
- def to_dict(self) -> Dict:
65
- return {
66
- "level": self.level.value,
67
- "metric": self.metric,
68
- "message": self.message,
69
- "value": self.value,
70
- "threshold": self.threshold,
71
- "timestamp": self.timestamp,
72
- }
73
-
74
-
75
- @dataclass
76
- class Snapshot:
77
- """Dashboard snapshot for comparison."""
78
- id: str
79
- name: str
80
- timestamp: str
81
- metrics: Dict[str, Any]
82
- alerts: List[Dict]
83
-
84
- def to_dict(self) -> Dict:
85
- return {
86
- "id": self.id,
87
- "name": self.name,
88
- "timestamp": self.timestamp,
89
- "metrics": self.metrics,
90
- "alerts": self.alerts,
91
- }
92
-
93
-
94
- class MetricsCache:
95
- """Cache with configurable TTL for dashboard metrics."""
96
-
97
- def __init__(self, ttl_ms: int = 100):
98
- self.ttl_ms = ttl_ms
99
- self.cache: Dict[str, Tuple[float, Any]] = {}
100
- self.lock = threading.RLock()
101
-
102
- def get(self, key: str) -> Optional[Any]:
103
- """Get cached value if not expired."""
104
- with self.lock:
105
- if key in self.cache:
106
- ts, value = self.cache[key]
107
- if (time.time() - ts) * 1000 < self.ttl_ms:
108
- return value
109
- del self.cache[key]
110
- return None
111
-
112
- def set(self, key: str, value: Any) -> None:
113
- """Set cached value with current timestamp."""
114
- with self.lock:
115
- self.cache[key] = (time.time(), value)
116
-
117
- def invalidate(self, key: str = None) -> None:
118
- """Invalidate cache entry or all entries."""
119
- with self.lock:
120
- if key:
121
- self.cache.pop(key, None)
122
- else:
123
- self.cache.clear()
124
-
125
-
126
- class AlertEngine:
127
- """Alert engine with configurable thresholds."""
128
-
129
- DEFAULT_THRESHOLDS = {
130
- "agents.exhausted_ratio": {"warning": 0.5, "critical": 0.9},
131
- "agents.utilization": {"warning": 0.9, "critical": 0.99},
132
- "tasks.pending": {"warning": 100, "critical": 500},
133
- "tasks.blocked_ratio": {"warning": 0.3, "critical": 0.5},
134
- "cost.budget_remaining_ratio": {"warning": 0.2, "critical": 0.05},
135
- "deps.max_depth": {"warning": 5, "critical": 10},
136
- "deps.circular": {"warning": 1, "critical": 1},
137
- "system.error_rate": {"warning": 5, "critical": 20},
138
- }
139
-
140
- def __init__(self):
141
- self.thresholds = dict(self.DEFAULT_THRESHOLDS)
142
- self.active_alerts: List[Alert] = []
143
-
144
- def set_threshold(self, metric: str, level: str, value: float) -> None:
145
- """Set custom threshold for metric."""
146
- if metric not in self.thresholds:
147
- self.thresholds[metric] = {}
148
- self.thresholds[metric][level] = value
149
-
150
- def check(self, metrics: Dict[str, Any]) -> List[Alert]:
151
- """Check all metrics against thresholds."""
152
- alerts = []
153
-
154
- # Agent alerts
155
- if "agents" in metrics:
156
- agents = metrics["agents"]
157
- total = agents.get("total", 0)
158
- exhausted = agents.get("exhausted", 0)
159
-
160
- if total > 0:
161
- exhausted_ratio = exhausted / total
162
- alert = self._check_threshold(
163
- "agents.exhausted_ratio", exhausted_ratio,
164
- f"{int(exhausted_ratio*100)}% of agents exhausted"
165
- )
166
- if alert:
167
- alerts.append(alert)
168
-
169
- utilization = agents.get("utilization", 0)
170
- alert = self._check_threshold(
171
- "agents.utilization", utilization,
172
- f"Agent pool at {int(utilization*100)}% capacity"
173
- )
174
- if alert:
175
- alerts.append(alert)
176
-
177
- # Task alerts
178
- if "tasks" in metrics:
179
- tasks = metrics["tasks"]
180
- pending = tasks.get("pending", 0)
181
-
182
- alert = self._check_threshold(
183
- "tasks.pending", pending,
184
- f"{pending} tasks pending in queue"
185
- )
186
- if alert:
187
- alerts.append(alert)
188
-
189
- total = tasks.get("total", 0)
190
- blocked = tasks.get("blocked", 0)
191
- if total > 0:
192
- blocked_ratio = blocked / total
193
- alert = self._check_threshold(
194
- "tasks.blocked_ratio", blocked_ratio,
195
- f"{int(blocked_ratio*100)}% of tasks blocked"
196
- )
197
- if alert:
198
- alerts.append(alert)
199
-
200
- # Cost alerts
201
- if "cost" in metrics:
202
- cost = metrics["cost"]
203
- budget = cost.get("budget", 0)
204
- remaining = cost.get("remaining", 0)
205
-
206
- if budget > 0:
207
- remaining_ratio = remaining / budget
208
- alert = self._check_threshold(
209
- "cost.budget_remaining_ratio", remaining_ratio,
210
- f"Only {int(remaining_ratio*100)}% budget remaining",
211
- inverse=True # Lower is worse
212
- )
213
- if alert:
214
- alerts.append(alert)
215
-
216
- # Dependency alerts
217
- if "deps" in metrics:
218
- deps = metrics["deps"]
219
- max_depth = deps.get("max_depth", 0)
220
-
221
- alert = self._check_threshold(
222
- "deps.max_depth", max_depth,
223
- f"Dependency chain depth of {max_depth}"
224
- )
225
- if alert:
226
- alerts.append(alert)
227
-
228
- circular = deps.get("circular", 0)
229
- if circular > 0:
230
- alerts.append(Alert(
231
- level=AlertLevel.CRITICAL,
232
- metric="deps.circular",
233
- message=f"{circular} circular dependencies detected!",
234
- value=circular,
235
- threshold=0,
236
- ))
237
-
238
- self.active_alerts = alerts
239
- return alerts
240
-
241
- def _check_threshold(
242
- self, metric: str, value: float, message: str, inverse: bool = False
243
- ) -> Optional[Alert]:
244
- """Check single metric against thresholds."""
245
- thresholds = self.thresholds.get(metric, {})
246
-
247
- critical_threshold = thresholds.get("critical")
248
- warning_threshold = thresholds.get("warning")
249
-
250
- if inverse:
251
- # Lower values are worse (e.g., budget remaining)
252
- if critical_threshold is not None and value <= critical_threshold:
253
- return Alert(AlertLevel.CRITICAL, metric, message, value, critical_threshold)
254
- if warning_threshold is not None and value <= warning_threshold:
255
- return Alert(AlertLevel.WARNING, metric, message, value, warning_threshold)
256
- else:
257
- # Higher values are worse (e.g., pending tasks)
258
- if critical_threshold is not None and value >= critical_threshold:
259
- return Alert(AlertLevel.CRITICAL, metric, message, value, critical_threshold)
260
- if warning_threshold is not None and value >= warning_threshold:
261
- return Alert(AlertLevel.WARNING, metric, message, value, warning_threshold)
262
-
263
- return None
264
-
265
- def get_active_alerts(self) -> List[Alert]:
266
- """Get currently active alerts."""
267
- return self.active_alerts
268
-
269
-
270
- class TrendAnalyzer:
271
- """Trend analyzer with JSONL persistence."""
272
-
273
- def __init__(self, brain_path: Path = None, retention_days: int = 7):
274
- self.brain_path = brain_path
275
- self.retention_days = retention_days
276
- self.metrics_file = None
277
- if brain_path:
278
- self.metrics_file = brain_path / "ledger" / "metrics.jsonl"
279
-
280
- def record_metrics(self, metrics: Dict[str, Any], interval: str = "hourly") -> None:
281
- """Record metrics to JSONL file."""
282
- if not self.metrics_file:
283
- return
284
-
285
- self.metrics_file.parent.mkdir(parents=True, exist_ok=True)
286
-
287
- entry = {
288
- "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
289
- "interval": interval,
290
- "metrics": self._flatten_metrics(metrics),
291
- }
292
-
293
- with open(self.metrics_file, "a") as f:
294
- f.write(json.dumps(entry) + "\n")
295
-
296
- self._cleanup_old_entries()
297
-
298
- def get_trends(self, metric: str, hours: int = 24) -> List[Dict]:
299
- """Get trend data for a specific metric."""
300
- if not self.metrics_file or not self.metrics_file.exists():
301
- return []
302
-
303
- cutoff = datetime.utcnow() - timedelta(hours=hours)
304
- trends = []
305
-
306
- with open(self.metrics_file) as f:
307
- for line in f:
308
- try:
309
- entry = json.loads(line.strip())
310
- ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00"))
311
- if ts.replace(tzinfo=None) >= cutoff:
312
- value = entry["metrics"].get(metric)
313
- if value is not None:
314
- trends.append({
315
- "timestamp": entry["timestamp"],
316
- "value": value,
317
- })
318
- except (json.JSONDecodeError, KeyError, ValueError):
319
- continue
320
-
321
- return trends
322
-
323
- def get_velocity(self, hours: int = 24) -> float:
324
- """Calculate task completion velocity (tasks/hour)."""
325
- trends = self.get_trends("tasks.done", hours)
326
- if len(trends) < 2:
327
- return 0.0
328
-
329
- first = trends[0]["value"]
330
- last = trends[-1]["value"]
331
- delta = last - first
332
-
333
- return delta / hours if hours > 0 else 0.0
334
-
335
- def _flatten_metrics(self, metrics: Dict, prefix: str = "") -> Dict[str, Any]:
336
- """Flatten nested metrics dict to dot-notation keys."""
337
- result = {}
338
- for key, value in metrics.items():
339
- full_key = f"{prefix}.{key}" if prefix else key
340
- if isinstance(value, dict):
341
- result.update(self._flatten_metrics(value, full_key))
342
- else:
343
- result[full_key] = value
344
- return result
345
-
346
- def _cleanup_old_entries(self) -> None:
347
- """Remove entries older than retention period."""
348
- if not self.metrics_file or not self.metrics_file.exists():
349
- return
350
-
351
- cutoff = datetime.utcnow() - timedelta(days=self.retention_days)
352
- kept_lines = []
353
-
354
- with open(self.metrics_file) as f:
355
- for line in f:
356
- try:
357
- entry = json.loads(line.strip())
358
- ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00"))
359
- if ts.replace(tzinfo=None) >= cutoff:
360
- kept_lines.append(line)
361
- except (json.JSONDecodeError, KeyError, ValueError):
362
- continue
363
-
364
- with open(self.metrics_file, "w") as f:
365
- f.writelines(kept_lines)
366
-
367
-
368
- class SnapshotManager:
369
- """Manage dashboard snapshots for comparison."""
370
-
371
- def __init__(self, brain_path: Path = None, max_snapshots: int = 100):
372
- self.brain_path = brain_path
373
- self.max_snapshots = max_snapshots
374
- self.snapshots_dir = None
375
- if brain_path:
376
- self.snapshots_dir = brain_path / "snapshots" / "dashboard"
377
-
378
- def create(self, metrics: Dict, alerts: List[Alert], name: str = None) -> Snapshot:
379
- """Create a new snapshot."""
380
- snapshot_id = f"snap_{int(time.time())}_{hashlib.sha256(str(time.time()).encode()).hexdigest()[:6]}"
381
- timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
382
-
383
- snapshot = Snapshot(
384
- id=snapshot_id,
385
- name=name or f"Snapshot {timestamp}",
386
- timestamp=timestamp,
387
- metrics=metrics,
388
- alerts=[a.to_dict() for a in alerts],
389
- )
390
-
391
- if self.snapshots_dir:
392
- self.snapshots_dir.mkdir(parents=True, exist_ok=True)
393
- snapshot_file = self.snapshots_dir / f"{snapshot_id}.json"
394
- with open(snapshot_file, "w") as f:
395
- json.dump(snapshot.to_dict(), f, indent=2)
396
-
397
- self._cleanup_old_snapshots()
398
-
399
- return snapshot
400
-
401
- def get(self, snapshot_id: str) -> Optional[Snapshot]:
402
- """Get a snapshot by ID."""
403
- if not self.snapshots_dir:
404
- return None
405
-
406
- snapshot_file = self.snapshots_dir / f"{snapshot_id}.json"
407
- if not snapshot_file.exists():
408
- return None
409
-
410
- with open(snapshot_file) as f:
411
- data = json.load(f)
412
-
413
- return Snapshot(**data)
414
-
415
- def list(self, limit: int = 10) -> List[Dict]:
416
- """List recent snapshots."""
417
- if not self.snapshots_dir or not self.snapshots_dir.exists():
418
- return []
419
-
420
- snapshots = []
421
- for f in sorted(self.snapshots_dir.glob("snap_*.json"), reverse=True)[:limit]:
422
- with open(f) as file:
423
- data = json.load(file)
424
- snapshots.append({
425
- "id": data["id"],
426
- "name": data["name"],
427
- "timestamp": data["timestamp"],
428
- })
429
-
430
- return snapshots
431
-
432
- def compare(self, snapshot_a: str, snapshot_b: str) -> Dict:
433
- """Compare two snapshots."""
434
- a = self.get(snapshot_a)
435
- b = self.get(snapshot_b)
436
-
437
- if not a or not b:
438
- return {"error": "Snapshot not found"}
439
-
440
- comparison = {
441
- "a": {"id": a.id, "timestamp": a.timestamp},
442
- "b": {"id": b.id, "timestamp": b.timestamp},
443
- "deltas": {},
444
- }
445
-
446
- # Compare flat metrics
447
- a_flat = self._flatten(a.metrics)
448
- b_flat = self._flatten(b.metrics)
449
-
450
- all_keys = set(a_flat.keys()) | set(b_flat.keys())
451
- for key in all_keys:
452
- a_val = a_flat.get(key, 0)
453
- b_val = b_flat.get(key, 0)
454
-
455
- if isinstance(a_val, (int, float)) and isinstance(b_val, (int, float)):
456
- delta = b_val - a_val
457
- if delta != 0:
458
- comparison["deltas"][key] = {
459
- "a": a_val,
460
- "b": b_val,
461
- "delta": delta,
462
- "direction": "up" if delta > 0 else "down",
463
- }
464
-
465
- return comparison
466
-
467
- def _flatten(self, d: Dict, prefix: str = "") -> Dict:
468
- """Flatten nested dict."""
469
- result = {}
470
- for k, v in d.items():
471
- key = f"{prefix}.{k}" if prefix else k
472
- if isinstance(v, dict):
473
- result.update(self._flatten(v, key))
474
- else:
475
- result[key] = v
476
- return result
477
-
478
- def _cleanup_old_snapshots(self) -> None:
479
- """Remove old snapshots beyond max limit."""
480
- if not self.snapshots_dir:
481
- return
482
-
483
- snapshots = sorted(self.snapshots_dir.glob("snap_*.json"))
484
- while len(snapshots) > self.max_snapshots:
485
- oldest = snapshots.pop(0)
486
- oldest.unlink()
487
-
488
-
489
- class ASCIIFormatter:
490
- """Format dashboard as ASCII art for terminal display."""
491
-
492
- LEVEL_ICONS = {
493
- "minimal": "📊",
494
- "standard": "🚀",
495
- "verbose": "📈",
496
- "full": "🔬",
497
- }
498
-
499
- def format(
500
- self,
501
- metrics: Dict[str, Any],
502
- alerts: List[Alert],
503
- detail_level: str = "standard",
504
- ) -> str:
505
- """Format metrics as ASCII dashboard."""
506
- lines = []
507
- now = time.strftime("%Y-%m-%d %H:%M:%S")
508
- icon = self.LEVEL_ICONS.get(detail_level, "📊")
509
-
510
- lines.append(f"{icon} NOP Status Dashboard - {now}")
511
- lines.append("═" * 60)
512
-
513
- # Alerts section (always shown if any)
514
- if alerts:
515
- lines.append("")
516
- lines.append("⚠️ ALERTS")
517
- lines.append("─" * 40)
518
- for alert in alerts:
519
- icon = "🔴" if alert.level == AlertLevel.CRITICAL else "🟡"
520
- lines.append(f" {icon} [{alert.level.value.upper()}] {alert.message}")
521
- lines.append("")
522
-
523
- # Agent Pool Health
524
- if "agents" in metrics:
525
- lines.extend(self._format_agents(metrics["agents"], detail_level))
526
-
527
- # Task Queue
528
- if "tasks" in metrics:
529
- lines.extend(self._format_tasks(metrics["tasks"], detail_level))
530
-
531
- # Ingestion (verbose/full only)
532
- if "ingestion" in metrics and detail_level in ["verbose", "full"]:
533
- lines.extend(self._format_ingestion(metrics["ingestion"]))
534
-
535
- # Cost Tracking
536
- if "cost" in metrics:
537
- lines.extend(self._format_cost(metrics["cost"], detail_level))
538
-
539
- # Dependencies (verbose/full only)
540
- if "deps" in metrics and detail_level in ["verbose", "full"]:
541
- lines.extend(self._format_deps(metrics["deps"]))
542
-
543
- # System Health (full only)
544
- if "system" in metrics and detail_level == "full":
545
- lines.extend(self._format_system(metrics["system"]))
546
-
547
- return "\n".join(lines)
548
-
549
- def _format_agents(self, agents: Dict, detail_level: str) -> List[str]:
550
- """Format agent pool section."""
551
- lines = ["", "📊 AGENT POOL HEALTH"]
552
- lines.append("─" * 40)
553
-
554
- total = agents.get("total", 0)
555
- active = agents.get("active", 0)
556
- idle = agents.get("idle", 0)
557
- exhausted = agents.get("exhausted", 0)
558
- utilization = agents.get("utilization", 0)
559
-
560
- pct = f"({int(utilization*100)}%)" if total > 0 else ""
561
- lines.append(f" ├── Active: {active}/{total} {pct}")
562
- lines.append(f" ├── Idle: {idle}")
563
- lines.append(f" ├── Exhausted: {exhausted}")
564
-
565
- if detail_level in ["verbose", "full"]:
566
- reset_warnings = agents.get("reset_warnings", [])
567
- if reset_warnings:
568
- lines.append(f" └── Reset Warnings: {len(reset_warnings)}")
569
- for w in reset_warnings[:3]:
570
- lines.append(f" └── {w.get('slot_id', 'unknown')} in {w.get('minutes', '?')}m")
571
- else:
572
- lines.append(" └── Reset Warnings: 0")
573
- else:
574
- lines.append(f" └── Capacity: {int(utilization*100)}%")
575
-
576
- return lines
577
-
578
- def _format_tasks(self, tasks: Dict, detail_level: str) -> List[str]:
579
- """Format task queue section."""
580
- lines = ["", "📋 TASK QUEUE"]
581
- lines.append("─" * 40)
582
-
583
- pending = tasks.get("pending", 0)
584
- in_progress = tasks.get("in_progress", 0)
585
- blocked = tasks.get("blocked", 0)
586
- done = tasks.get("done", 0)
587
- failed = tasks.get("failed", 0)
588
- velocity = tasks.get("velocity", 0)
589
-
590
- lines.append(f" ├── Pending: {pending}")
591
- lines.append(f" ├── In Progress: {in_progress}")
592
- lines.append(f" ├── Blocked: {blocked}")
593
-
594
- if detail_level in ["verbose", "full"]:
595
- lines.append(f" ├── Done (24h): {done}")
596
- lines.append(f" ├── Failed: {failed}")
597
- lines.append(f" └── Velocity: {velocity:.1f}/hr")
598
- else:
599
- lines.append(f" └── Done (24h): {done}")
600
-
601
- return lines
602
-
603
- def _format_ingestion(self, ingestion: Dict) -> List[str]:
604
- """Format ingestion section."""
605
- lines = ["", "📥 INGESTION"]
606
- lines.append("─" * 40)
607
-
608
- total = ingestion.get("total", 0)
609
- skipped = ingestion.get("skipped", 0)
610
- failed = ingestion.get("failed", 0)
611
- batches = ingestion.get("batches", 0)
612
-
613
- lines.append(f" ├── Total Ingested: {total}")
614
- lines.append(f" ├── Skipped (Dupes): {skipped}")
615
- lines.append(f" ├── Failed: {failed}")
616
- lines.append(f" └── Batches: {batches}")
617
-
618
- return lines
619
-
620
- def _format_cost(self, cost: Dict, detail_level: str) -> List[str]:
621
- """Format cost tracking section."""
622
- lines = ["", "💰 COST TRACKING"]
623
- lines.append("─" * 40)
624
-
625
- tokens = cost.get("tokens", 0)
626
- usd = cost.get("usd", 0)
627
- budget = cost.get("budget", 0)
628
- remaining = cost.get("remaining", 0)
629
- burn_rate = cost.get("burn_rate", 0)
630
-
631
- # Format tokens
632
- if tokens >= 1_000_000:
633
- tokens_str = f"{tokens/1_000_000:.1f}M"
634
- elif tokens >= 1_000:
635
- tokens_str = f"{tokens/1_000:.1f}K"
636
- else:
637
- tokens_str = str(tokens)
638
-
639
- lines.append(f" ├── Tokens: {tokens_str}")
640
- lines.append(f" ├── Estimated: ${usd:.2f}")
641
-
642
- if budget > 0:
643
- pct = int((remaining / budget) * 100)
644
- lines.append(f" ├── Budget: ${budget:.2f} ({pct}% remaining)")
645
-
646
- if detail_level in ["verbose", "full"]:
647
- lines.append(f" └── Burn Rate: ${burn_rate:.2f}/hr")
648
- else:
649
- lines.append(f" └── Remaining: ${remaining:.2f}")
650
-
651
- return lines
652
-
653
- def _format_deps(self, deps: Dict) -> List[str]:
654
- """Format dependency section."""
655
- lines = ["", "🔗 DEPENDENCIES"]
656
- lines.append("─" * 40)
657
-
658
- max_depth = deps.get("max_depth", 0)
659
- blocked_chains = deps.get("blocked_chains", 0)
660
- circular = deps.get("circular", 0)
661
-
662
- lines.append(f" ├── Max Depth: {max_depth}")
663
- lines.append(f" ├── Blocked Chains: {blocked_chains}")
664
- lines.append(f" └── Circular: {circular}")
665
-
666
- return lines
667
-
668
- def _format_system(self, system: Dict) -> List[str]:
669
- """Format system health section."""
670
- lines = ["", "🖥️ SYSTEM HEALTH"]
671
- lines.append("─" * 40)
672
-
673
- uptime = system.get("uptime", "N/A")
674
- last_activity = system.get("last_activity", "N/A")
675
- error_rate = system.get("error_rate", 0)
676
-
677
- lines.append(f" ├── Uptime: {uptime}")
678
- lines.append(f" ├── Last Activity: {last_activity}")
679
- lines.append(f" └── Error Rate: {error_rate}/hr")
680
-
681
- return lines
682
-
683
-
684
- class JSONFormatter:
685
- """Format dashboard as JSON for API consumption."""
686
-
687
- def format(
688
- self,
689
- metrics: Dict[str, Any],
690
- alerts: List[Alert],
691
- detail_level: str = "standard",
692
- ) -> str:
693
- """Format metrics as JSON."""
694
- output = {
695
- "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
696
- "detail_level": detail_level,
697
- "metrics": metrics,
698
- "alerts": [a.to_dict() for a in alerts],
699
- }
700
- return json.dumps(output, indent=2)
701
-
702
-
703
- class MermaidFormatter:
704
- """Format dependency graph as Mermaid diagram."""
705
-
706
- def format(self, deps: Dict) -> str:
707
- """Format dependencies as Mermaid diagram."""
708
- lines = ["```mermaid", "graph TD"]
709
-
710
- forward_deps = deps.get("forward_deps", {})
711
- depths = deps.get("depths", {})
712
-
713
- # Add nodes with depth-based styling
714
- nodes_added = set()
715
- for task_id, task_deps in forward_deps.items():
716
- if task_id not in nodes_added:
717
- depth = depths.get(task_id, 0)
718
- style = self._get_node_style(depth)
719
- lines.append(f" {self._safe_id(task_id)}{style}")
720
- nodes_added.add(task_id)
721
-
722
- for dep_id in task_deps:
723
- if dep_id not in nodes_added:
724
- depth = depths.get(dep_id, 0)
725
- style = self._get_node_style(depth)
726
- lines.append(f" {self._safe_id(dep_id)}{style}")
727
- nodes_added.add(dep_id)
728
-
729
- lines.append(f" {self._safe_id(dep_id)} --> {self._safe_id(task_id)}")
730
-
731
- lines.append("```")
732
- return "\n".join(lines)
733
-
734
- def _safe_id(self, task_id: str) -> str:
735
- """Convert task ID to safe Mermaid node ID."""
736
- return task_id.replace("-", "_").replace(".", "_")
737
-
738
- def _get_node_style(self, depth: int) -> str:
739
- """Get node style based on depth."""
740
- if depth == 0:
741
- return "[🟢 Root]"
742
- elif depth <= 2:
743
- return f"[Depth {depth}]"
744
- elif depth <= 4:
745
- return f"[⚠️ Depth {depth}]"
746
- else:
747
- return f"[🔴 Depth {depth}]"
748
-
749
-
750
- class MetricsCollector:
751
- """Collect metrics from all NOP V3.1 components."""
752
-
753
- def __init__(self, orchestrator=None, brain_path: Path = None):
754
- self.orch = orchestrator
755
- self.brain_path = brain_path
756
- self.cache = MetricsCache(ttl_ms=100)
757
-
758
- def collect_all(self, use_cache: bool = True) -> Dict[str, Any]:
759
- """Collect all metrics from all components."""
760
- if use_cache:
761
- cached = self.cache.get("all_metrics")
762
- if cached:
763
- return cached
764
-
765
- metrics = {
766
- "agents": self.collect_agent_metrics(),
767
- "tasks": self.collect_task_metrics(),
768
- "ingestion": self.collect_ingestion_metrics(),
769
- "cost": self.collect_cost_metrics(),
770
- "deps": self.collect_dependency_metrics(),
771
- "system": self.collect_system_metrics(),
772
- }
773
-
774
- if use_cache:
775
- self.cache.set("all_metrics", metrics)
776
-
777
- return metrics
778
-
779
- def collect_agent_metrics(self) -> Dict:
780
- """Collect agent pool metrics."""
781
- try:
782
- if self.orch:
783
- pool = self.orch.get_agent_pool()
784
- if pool:
785
- pool_metrics = pool.get_pool_metrics() if hasattr(pool, 'get_pool_metrics') else {}
786
- return {
787
- "total": pool_metrics.get("total_agents", 0),
788
- "active": pool_metrics.get("active_agents", 0),
789
- "idle": pool_metrics.get("idle_agents", 0),
790
- "exhausted": pool_metrics.get("exhausted_agents", 0),
791
- "utilization": pool_metrics.get("utilization", 0),
792
- "reset_warnings": [],
793
- }
794
-
795
- # Fallback: read from registry
796
- if self.brain_path:
797
- registry_path = self.brain_path / "slots" / "registry.json"
798
- if registry_path.exists():
799
- with open(registry_path) as f:
800
- registry = json.load(f)
801
-
802
- slots = registry.get("slots", {})
803
- total = len(slots)
804
- active = sum(1 for s in slots.values() if s.get("status") == "active")
805
- exhausted = sum(1 for s in slots.values() if s.get("status") == "exhausted")
806
- idle = active # Approximate
807
-
808
- return {
809
- "total": total,
810
- "active": active,
811
- "idle": idle,
812
- "exhausted": exhausted,
813
- "utilization": active / total if total > 0 else 0,
814
- "reset_warnings": [],
815
- }
816
-
817
- return {"error": "Agent metrics unavailable"}
818
- except Exception as e:
819
- return {"error": str(e)}
820
-
821
- def collect_task_metrics(self) -> Dict:
822
- """Collect task queue metrics."""
823
- try:
824
- if self.orch:
825
- pool_metrics = self.orch.get_pool_metrics()
826
- return {
827
- "total": pool_metrics.get("total_tasks", 0),
828
- "pending": pool_metrics.get("pending", 0),
829
- "in_progress": pool_metrics.get("in_progress", 0),
830
- "blocked": pool_metrics.get("blocked", 0),
831
- "done": pool_metrics.get("done", 0),
832
- "failed": pool_metrics.get("failed", 0),
833
- "velocity": 0, # Calculated from trends
834
- }
835
-
836
- # Fallback: read from tasks.json
837
- if self.brain_path:
838
- tasks_path = self.brain_path / "ledger" / "tasks.json"
839
- if tasks_path.exists():
840
- with open(tasks_path) as f:
841
- data = json.load(f)
842
-
843
- tasks = data.get("tasks", [])
844
- return {
845
- "total": len(tasks),
846
- "pending": sum(1 for t in tasks if t.get("status") in ["PENDING", "READY"]),
847
- "in_progress": sum(1 for t in tasks if t.get("status") == "IN_PROGRESS"),
848
- "blocked": sum(1 for t in tasks if t.get("status") == "BLOCKED"),
849
- "done": sum(1 for t in tasks if t.get("status") == "DONE"),
850
- "failed": sum(1 for t in tasks if t.get("status") == "FAILED"),
851
- "velocity": 0,
852
- }
853
-
854
- return {"error": "Task metrics unavailable"}
855
- except Exception as e:
856
- return {"error": str(e)}
857
-
858
- def collect_ingestion_metrics(self) -> Dict:
859
- """Collect ingestion statistics."""
860
- try:
861
- if self.orch:
862
- stats = self.orch.get_ingestion_stats()
863
- return {
864
- "total": stats.get("total_ingested", 0),
865
- "skipped": stats.get("total_skipped", 0),
866
- "failed": stats.get("total_failed", 0),
867
- "batches": stats.get("batches_count", 0),
868
- "by_source": stats.get("by_source", {}),
869
- }
870
-
871
- return {
872
- "total": 0,
873
- "skipped": 0,
874
- "failed": 0,
875
- "batches": 0,
876
- "by_source": {},
877
- }
878
- except Exception as e:
879
- return {"error": str(e)}
880
-
881
- def collect_cost_metrics(self) -> Dict:
882
- """Collect cost tracking metrics."""
883
- # This would integrate with LLM client cost tracking
884
- return {
885
- "tokens": 0,
886
- "usd": 0.0,
887
- "budget": 10.0,
888
- "remaining": 10.0,
889
- "burn_rate": 0.0,
890
- }
891
-
892
- def collect_dependency_metrics(self) -> Dict:
893
- """Collect dependency graph metrics."""
894
- try:
895
- if self.orch:
896
- graph = self.orch.get_dependency_graph()
897
- depths = graph.get("depths", {})
898
-
899
- max_depth = max(depths.values()) if depths else 0
900
- circular = sum(1 for d in depths.values() if d < 0)
901
- blocked_chains = sum(1 for d in depths.values() if d > 3)
902
-
903
- return {
904
- "max_depth": max_depth,
905
- "blocked_chains": blocked_chains,
906
- "circular": circular,
907
- }
908
-
909
- return {"max_depth": 0, "blocked_chains": 0, "circular": 0}
910
- except Exception as e:
911
- return {"error": str(e)}
912
-
913
- def collect_system_metrics(self) -> Dict:
914
- """Collect system health metrics."""
915
- return {
916
- "uptime": "N/A",
917
- "last_activity": time.strftime("%Y-%m-%d %H:%M:%S"),
918
- "error_rate": 0,
919
- }
920
-
921
-
922
- class DashboardEngine:
923
- """
924
- Enterprise-grade orchestration dashboard.
925
-
926
- Provides real-time visibility into all NOP V3.1 components
927
- with multiple output formats, alerting, and trend analysis.
928
- """
929
-
930
- def __init__(
931
- self,
932
- orchestrator=None,
933
- brain_path: Path = None,
934
- ):
935
- self.orch = orchestrator
936
- self.brain_path = brain_path
937
-
938
- # Initialize components
939
- self.collector = MetricsCollector(orchestrator, brain_path)
940
- self.alert_engine = AlertEngine()
941
- self.trend_analyzer = TrendAnalyzer(brain_path)
942
- self.snapshot_manager = SnapshotManager(brain_path)
943
-
944
- # Initialize formatters
945
- self.formatters = {
946
- OutputFormat.ASCII: ASCIIFormatter(),
947
- OutputFormat.JSON: JSONFormatter(),
948
- OutputFormat.MERMAID: MermaidFormatter(),
949
- }
950
-
951
- def render(
952
- self,
953
- detail_level: str = "standard",
954
- format: str = "ascii",
955
- include_alerts: bool = True,
956
- include_trends: bool = False,
957
- compare_to: str = None,
958
- category: str = None,
959
- ) -> str:
960
- """
961
- Render the dashboard in the specified format.
962
-
963
- Args:
964
- detail_level: "minimal", "standard", "verbose", "full"
965
- format: "ascii", "json", "mermaid"
966
- include_alerts: Include alert section
967
- include_trends: Include trend data
968
- compare_to: Snapshot ID or time offset ("1h", "24h")
969
- category: Filter to specific category
970
-
971
- Returns:
972
- Formatted dashboard string
973
- """
974
- # Collect metrics
975
- metrics = self.collector.collect_all()
976
-
977
- # Filter by category if specified
978
- if category and category in metrics:
979
- metrics = {category: metrics[category]}
980
-
981
- # Add trends if requested
982
- if include_trends:
983
- velocity = self.trend_analyzer.get_velocity(hours=24)
984
- if "tasks" in metrics:
985
- metrics["tasks"]["velocity"] = velocity
986
-
987
- # Check alerts
988
- alerts = self.alert_engine.check(metrics) if include_alerts else []
989
-
990
- # Handle comparison
991
- if compare_to:
992
- return self._render_comparison(metrics, compare_to)
993
-
994
- # Format output
995
- output_format = OutputFormat(format.lower())
996
-
997
- if output_format == OutputFormat.MERMAID:
998
- deps = metrics.get("deps", {})
999
- if self.orch:
1000
- deps = self.orch.get_dependency_graph()
1001
- return self.formatters[output_format].format(deps)
1002
-
1003
- formatter = self.formatters.get(output_format, self.formatters[OutputFormat.ASCII])
1004
- return formatter.format(metrics, alerts, detail_level)
1005
-
1006
- def _render_comparison(self, current_metrics: Dict, compare_to: str) -> str:
1007
- """Render comparison view."""
1008
- # Parse compare_to
1009
- if compare_to.endswith("h"):
1010
- # Time offset comparison
1011
- hours = int(compare_to[:-1])
1012
- # For now, just show current - would need historical snapshots
1013
- lines = [
1014
- f"📊 Dashboard Comparison: Now vs {hours}h ago",
1015
- "═" * 50,
1016
- "(Historical comparison requires snapshot data)",
1017
- ]
1018
- return "\n".join(lines)
1019
- else:
1020
- # Snapshot comparison
1021
- comparison = self.snapshot_manager.compare("current", compare_to)
1022
- if "error" in comparison:
1023
- return f"❌ {comparison['error']}"
1024
-
1025
- lines = [
1026
- "📊 Dashboard Comparison",
1027
- "═" * 50,
1028
- ]
1029
- for key, delta in comparison.get("deltas", {}).items():
1030
- direction = "✅" if delta["direction"] == "up" else "⬇️"
1031
- lines.append(f" {key}: {delta['a']} → {delta['b']} ({direction} {abs(delta['delta'])})")
1032
-
1033
- return "\n".join(lines)
1034
-
1035
- def get_metrics(self, category: str = None) -> Dict:
1036
- """Get raw metrics dictionary."""
1037
- metrics = self.collector.collect_all()
1038
- if category and category in metrics:
1039
- return {category: metrics[category]}
1040
- return metrics
1041
-
1042
- def get_alerts(self) -> List[Alert]:
1043
- """Get current active alerts."""
1044
- metrics = self.collector.collect_all()
1045
- return self.alert_engine.check(metrics)
1046
-
1047
- def create_snapshot(self, name: str = None) -> Snapshot:
1048
- """Create a manual snapshot."""
1049
- metrics = self.collector.collect_all(use_cache=False)
1050
- alerts = self.alert_engine.check(metrics)
1051
- return self.snapshot_manager.create(metrics, alerts, name)
1052
-
1053
- def compare_snapshots(self, snapshot_a: str, snapshot_b: str) -> Dict:
1054
- """Compare two snapshots."""
1055
- return self.snapshot_manager.compare(snapshot_a, snapshot_b)
1056
-
1057
- def list_snapshots(self, limit: int = 10) -> List[Dict]:
1058
- """List available snapshots."""
1059
- return self.snapshot_manager.list(limit)
1060
-
1061
- def set_alert_threshold(self, metric: str, level: str, value: float) -> None:
1062
- """Set custom alert threshold."""
1063
- self.alert_engine.set_threshold(metric, level, value)
1064
-
1065
- def record_hourly_metrics(self) -> None:
1066
- """Record current metrics for trend analysis."""
1067
- metrics = self.collector.collect_all(use_cache=False)
1068
- self.trend_analyzer.record_metrics(metrics, interval="hourly")
1069
-
1070
- def get_trends(self, metric: str, hours: int = 24) -> List[Dict]:
1071
- """Get trend data for a metric."""
1072
- return self.trend_analyzer.get_trends(metric, hours)
1073
-
1074
-
1075
- def format_dashboard(
1076
- metrics: Dict,
1077
- alerts: List[Alert],
1078
- detail_level: str = "standard",
1079
- format: str = "ascii",
1080
- ) -> str:
1081
- """Standalone function for formatting dashboard output."""
1082
- if format.lower() == "ascii":
1083
- return ASCIIFormatter().format(metrics, alerts, detail_level)
1084
- elif format.lower() == "json":
1085
- return JSONFormatter().format(metrics, alerts, detail_level)
1086
- else:
1087
- return ASCIIFormatter().format(metrics, alerts, detail_level)