diff --git a/.clawmetry-fleet.db b/.clawmetry-fleet.db new file mode 100644 index 0000000..757d372 Binary files /dev/null and b/.clawmetry-fleet.db differ diff --git a/clawmetry-landing/pitch-deck.html b/clawmetry-landing/pitch-deck.html new file mode 100644 index 0000000..e150e64 --- /dev/null +++ b/clawmetry-landing/pitch-deck.html @@ -0,0 +1,1048 @@ + + + + + +ClawMetry — Investor Pitch Deck + + + + + + + +
+
+ 🦞 +
+

ClawMetry

+

+ The observability layer for autonomous AI agents +

+
+ Open Source + 95K+ Downloads + $5/node/mo Cloud +
+

+ Seed Round · March 2026 +

+
01 / 14
+
+ + + + +
+ + THE PROBLEM +

AI agents are a black box

+

+ OpenClaw is the fastest-growing AI agent framework. 316K GitHub stars, powering tens of thousands of autonomous agents worldwide. But once you deploy an agent, you have zero visibility. +

+ +
+
+
💸
+

Cost surprise

+

Agents make recursive LLM calls. A single loop can burn $50 in tokens before you notice. No real-time cost alerts exist.

+
+
+
👻
+

Invisible failures

+

Sub-agents spawn sub-agents. Cron jobs fail silently. Memory files bloat. You find out when something breaks in production.

+
+
+
🔒
+

No governance

+

What tools did the agent call? What data did it access? Which external APIs? Enterprises need audit trails. None exist.

+
+
+ +
+
"Ask OpenClaw to do something. It starts working. And you just... wait. Hoping. Is it searching? Which websites? Where is it stuck right now? Zero visibility."
+
— OpenClaw user, Reddit r/OpenClaw
+
+ +
02 / 14
+
+ + + + +
+ + WHY NOW +

The agent era just started

+ +
+
+
+
+
Jan 2025
+
OpenClaw launches, hits 100K GitHub stars in 2 months
+
+
+
Feb 2025
+
OpenClaw creator joins OpenAI. Ecosystem explodes.
+
+
+
Mar 2025
+
OpenClaw passes 200K stars. Karpathy demos it on a Mac mini.
+
+
+
2026
+
316K stars, 60K forks. The standard for personal AI agents.
+
+
+
Now
+
Enterprises adopting OpenClaw. No observability layer exists. That's us.
+
+
+
+ +
+
+

Market timing is everything

+

+ Every major platform shift creates a monitoring layer worth billions: +

+
+
+ Cloud → Datadog$44B market cap +
+
+ Containers → Kubernetes → PrometheusAcquired by CNCF +
+
+ LLM Apps → Langfuse, Helicone$30M+ raised +
+
+ AI Agents → ClawMetryYou're here +
+
+
+
+
+ +
03 / 14
+
+ + + + +
+ + THE SOLUTION +

See everything your agent does.
In real time.

+

+ ClawMetry is the purpose-built observability dashboard for AI agents running on OpenClaw. Install in 30 seconds. No config needed. +

+ +
+
+
🧠
+

Live Agent Flow

+

Watch your agent think in real time. See every tool call, sub-agent spawn, and decision as it happens. Full session traces with cost per step.

+
+
+
💰
+

Token Cost Dashboard

+

Real-time cost tracking per session, per model, per agent. Set budgets and alerts before a runaway loop drains your account.

+
+
+
+

Cron & Sub-Agent Health

+

Monitor every scheduled task and sub-agent with status, logs, and failure alerts. No more silent failures at 3 AM.

+
+
+
📝
+

Memory & Governance

+

Track what your agent remembers, which files it changes, and full audit trails of every action. Enterprise compliance ready.

+
+
+ +
04 / 14
+
+ + + + +
+ + PRODUCT +

One command. Full visibility.

+ +
+
Terminal
+
+ $ pip install clawmetry
+ $ clawmetry
+ ✔ Connected to OpenClaw workspace
+ ✔ Dashboard running at http://localhost:8900
+ ✔ Monitoring 3 active sessions, 12 cron jobs, 847 tool calls +
+
+ +
+
+

Local Dashboard (Free)

+
    +
  • ✅ Real-time session monitoring
  • +
  • ✅ Token cost tracking per model
  • +
  • ✅ Cron job health dashboard
  • +
  • ✅ Sub-agent tree visualization
  • +
  • ✅ Memory file change tracking
  • +
+
+
+

Cloud Dashboard ($5/node/mo)

+
    +
  • ✅ Everything in Local, plus:
  • +
  • ☁️ E2E encrypted cloud sync
  • +
  • ☁️ Multi-node monitoring
  • +
  • ☁️ Access from any browser, no VPN
  • +
  • ☁️ Team dashboards (coming)
  • +
+
+
+ +
05 / 14
+
+ + + + +
+ + TRACTION +

26 days. 95K downloads.

+

Zero paid marketing. Pure organic pull from the OpenClaw ecosystem.

+ +
+
+
95,794
+
PyPI Downloads
+
10K/week, accelerating
+
+
+
10K+
+
Weekly Active Installs
+
2K/day average
+
+
+
#5
+
Product Hunt Launch
+
198 upvotes, launch day
+
+
+
358
+
Email Subscribers
+
Organic signups
+
+
+
558
+
Cloud Dashboard Requests
+
Managed instance interest
+
+
+
27K+
+
Install Commands Copied
+
From landing page
+
+
+ +
+
"Installed OpenClaw. Hit the observability wall immediately. Then found ClawMetry. This should be built-in."
+
— OpenClaw community member
+
+ +
06 / 14
+
+ + + + +
+ + GROWTH +

Distribution advantage: ride the wave

+ +
+
+

Every OpenClaw user is a ClawMetry prospect

+

+ OpenClaw grows 10K+ stars/month. Each new user eventually needs observability. We're the only tool purpose-built for this ecosystem. +

+ +
+
+
OpenClaw
+
+
316K ★
+
+
+
+
PicoClaw
+
+
21K ★
+
+
+
+
NanoClaw
+
+
16K ★
+
+
+
+
Monitoring
+
+
Just us
+
+
+
+
+ +
+
+

Flywheel mechanics

+
+
+ 1. Open source gets adoption (95K+ downloads) +
+
+ 2. Local dashboard creates habit + dependency +
+
+ 3. Multi-node users need cloud sync → $5/node/mo +
+
+ 4. Teams need governance → enterprise tier +
+
+ 5. Community contributions improve the product (free R&D) +
+
+
+
+
+ +
07 / 14
+
+ + + + +
+ + MARKET +

$8B market by 2034

+

LLM observability is the fastest-growing segment in developer tools.

+ +
+
+
TAM
+
$8.1B
+
LLM Observability Market 2034
31.8% CAGR (Market.us)
+
+
+
SAM
+
$1.2B
+
AI Agent Monitoring
OpenClaw + adjacent frameworks
+
+
+
SOM
+
$120M
+
OpenClaw ecosystem
~100K active users at $100/yr avg
+
+
+ +
+

Market signals

+
+
+

📈 Langfuse: $4.5M raised, $1.1M ARR (7-person team)

+

Open-source LLM observability. YC + Lightspeed backed.

+
+
+

📈 Helicone: $5M seed at $25M valuation

+

LLM proxy/monitoring. Both validate the category.

+
+
+

📈 Dataiku & Monte Carlo launched agent monitoring (Mar 2026)

+

Enterprise players entering = massive market validation.

+
+
+

📈 OpenClaw ecosystem: 316K stars, growing 10K+/month

+

Fastest-growing agent framework. Our addressable base grows daily.

+
+
+
+ +
08 / 14
+
+ + + + +
+ + BUSINESS MODEL +

Open core. Free to love, paid to scale.

+ +
+
+
Free
+
Local Dashboard
+
+ ✅ Full local monitoring
+ ✅ All dashboard features
+ ✅ Community support
+ ✅ Open source (MIT)
+
+
+ Purpose: Adoption & habit +
+
+
+
$5/node/mo
+
Cloud Sync
+
+ ☁️ E2E encrypted sync
+ ☁️ Multi-node dashboard
+ ☁️ Browser access anywhere
+ ☁️ 7-day free trial
+
+
+ Purpose: Revenue engine +
+
+
+
Enterprise
+
Coming H2 2026
+
+ 🏢 SSO & RBAC
+ 🏢 Audit & compliance logs
+ 🏢 Cost governance & budgets
+ 🏢 On-prem deployment option
+
+
+ Purpose: Expand ARPU +
+
+
+ +
+

Unit economics

+
+
$5
ARPU/mo (today)
+
~$0.30
COGS/node/mo
+
94%
Gross margin
+
$0
CAC (organic)
+
+
+ +
09 / 14
+
+ + + + +
+ + COMPETITIVE LANDSCAPE +

Built for agents. Not retrofitted.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureClawMetryLangSmithLangfuseHeliconeDatadog
Agent-native (sub-agents, crons, memory)
OpenClaw integration (zero config)
Real-time agent flow visualizationPartial
Local-first (works offline, your data)
Open source MIT
Memory & workspace monitoring
30-second setupMinutesMinutesMinutesHours
Pricing$5/node/mo$39+/seatUsage-based$20+/mo$23+/host
+ +
+ Key insight: Existing tools monitor LLM API calls. ClawMetry monitors agents: the orchestration layer of sub-agents, cron jobs, tool use, memory, and session state that no API-level tool can see. +
+ +
10 / 14
+
+ + + + +
+ + ROADMAP +

From dashboard to control plane

+ +
+
+
+
✅ Shipped (Q1 2026)
+

Observability Dashboard

+

Local + cloud dashboard, live agent flow, cost tracking, cron health, memory monitoring. Mac app. 95K downloads.

+
+
+
🔨 Building (Q2 2026)
+

Alerting & Cost Controls

+

Real-time Slack/email/Telegram alerts on cost spikes, agent failures. Budget caps that auto-pause non-critical agents. Security posture dashboard.

+
+
+
+
+
🎯 Next (Q3 2026)
+

Multi-Framework Support

+

Expand beyond OpenClaw to AutoGPT, CrewAI, LangGraph. SDK for any agent framework. Become the universal agent observability layer.

+
+
+
🏢 Vision (Q4 2026+)
+

Enterprise Agent Governance

+

SSO, RBAC, audit logs, compliance reporting, cost allocation by team/project. The Datadog for AI agents.

+
+
+
+ +
11 / 14
+
+ + + + +
+ + TEAM +

Builder DNA

+ +
+
+

Vivek Chand

+

Founder & CEO

+

+ Former Senior Engineer at Booking.com, where he led AI-powered customer service summarization, scaling from 60% to 95%+ quality across 111K daily interactions. Built the evaluation framework, agentic loop, and production system saving €35M/year. +

+

+ B.E. Computer Science. Built ClawMetry in 2 weeks from personal frustration. Simultaneously runs VedicVoice (Sanskrit digital library) and InburgeringAI (Dutch exam prep, live users). +

+

+ Based in Almere, Netherlands 🇳🇱 +

+
+ +
+
+

Why this founder?

+
+ 🎯 Domain expert: Built AI observability at Booking.com scale
+ 🛠️ Full-stack builder: Ships fast, solo-built MVP in 2 weeks
+ 📊 Quality-obsessed: LLM-as-Judge evaluation + cost optimization
+ 🌍 Open source native: Community-first, understands developer GTM
+ 🔥 Dog-fooding: Uses ClawMetry daily to monitor his own AI agents +
+
+ +
+

Advisory & Network

+

+ Raphael Neuhaus — Ex-Google Cloud (AI & Startup Ecosystem Partnerships Lead, Benelux). Enterprise GTM advisor. LP Syndicate Investor, Co-Founder South Venture accelerator. +

+
+
+
+ +
12 / 14
+
+ + + + +
+ + THE ASK +

Raising €500K seed

+

+ To capture the AI agent observability market before incumbents catch up. +

+ +
+
+
40%
+
Engineering
+
Multi-framework SDK, alerting, enterprise features
+
+
+
30%
+
Go-to-Market
+
Developer advocacy, content, community
+
+
+
30%
+
Hiring
+
2 engineers (agent frameworks + infra)
+
+
+ +
+

18-month milestones

+
+
+ Month 6 + 500K+ downloads, alerting shipped, 500+ cloud nodes +
+
+ Month 12 + Multi-framework support, €10K MRR, enterprise pilots +
+
+ Month 18 + Series A ready: €50K+ MRR, enterprise contracts +
+
+
+ +
13 / 14
+
+ + + + +
+
+ 🦞 +
+

The Datadog for
AI Agents

+

+ 95K downloads in 26 days. Zero marketing spend. The only observability tool purpose-built for autonomous AI agents. Open source. Already loved. +

+ +
+
+ 🌐 clawmetry.com +
+
+ 📦 github.com/vivekchand/clawmetry +
+
+ 📧 vivek@clawmetry.com +
+
+ +

+ "Every platform shift creates a monitoring layer worth billions.
AI agents are the biggest platform shift of the decade." +

+ +
14 / 14
+
+ + + diff --git a/dashboard.py b/dashboard.py index 3bf081e..1995067 100755 --- a/dashboard.py +++ b/dashboard.py @@ -2577,6 +2577,7 @@ def get_local_ip(): + @@ -3405,6 +3406,90 @@ def get_local_ip(): + +
+
+
+

Quick Actions

+

Trigger common operations on this node without leaving the dashboard. Each action shows a confirmation dialog before executing.

+
+ + + + + +
+ + +
+
+
+
+
Restart Gateway
+
Restart the OpenClaw gateway process. Active sessions will reconnect automatically.
+ +
+
+
+ + +
+
+
🗑
+
+
Clear Cache
+
Purge ClawMetry in-process caches and local cache directories. Dashboard metrics will refresh from source.
+ +
+
+
+ + +
+
+
📄
+
+
Rotate Logs
+
Send SIGUSR1 to OpenClaw processes to trigger log rotation. Prevents unbounded log file growth.
+ +
+
+
+ + +
+
+
+
+
Health Check
+
Run an on-demand health check: gateway reachability, disk usage, session count.
+ +
+
+
+ +
+ + +
+
Recent Actions
+
Loading...
+
+
+
+ + + +
@@ -3717,6 +3802,7 @@ def get_local_ip(): if (name === 'brain') loadBrainPage(); if (name === 'security') { loadSecurityPage(); loadSecurityPosture(); } if (name === 'channels') loadChannelsPage(); + if (name === 'actions') loadQAHistory(); if (name === 'logs') { if (!logStream || logStream.readyState === EventSource.CLOSED) startLogStream(); loadLogs(); } } @@ -7159,6 +7245,7 @@ def get_local_ip(): + @@ -8033,6 +8120,65 @@ def get_local_ip():
+ +
+
+
+

Quick Actions

+

Trigger common operations on this node without leaving the dashboard.

+
+ +
+
+
+
+
+
Restart Gateway
+
Restart the OpenClaw gateway process.
+ +
+
+
+
+
+
🗑
+
+
Clear Cache
+
Purge ClawMetry in-process caches and local cache directories.
+ +
+
+
+
+
+
📄
+
+
Rotate Logs
+
Send SIGUSR1 to OpenClaw processes to trigger log rotation.
+ +
+
+
+
+
+
+
+
Health Check
+
Run an on-demand health check: gateway reachability, disk usage, session count.
+ +
+
+
+
+
+
Recent Actions
+
Loading...
+
+
+
+ + +
@@ -8372,6 +8518,7 @@ def get_local_ip(): if (name === 'security') { loadSecurityPage(); loadSecurityPosture(); } if (name === 'channels') loadChannelsPage(); if (name === 'logs') { if (!logStream || logStream.readyState === EventSource.CLOSED) startLogStream(); loadLogs(); } + if (name === 'actions') loadQAHistory(); } function exportUsageData() { @@ -9451,6 +9598,99 @@ def get_local_ip(): } } +// ═══ QUICK ACTIONS ═══════════════════════════════════════════════════════════ +var _qaCurrentAction = null; + +function confirmAction(actionKey, title, body) { + _qaCurrentAction = actionKey; + document.getElementById('qa-confirm-title').textContent = title; + document.getElementById('qa-confirm-body').textContent = body; + var overlay = document.getElementById('qa-confirm-overlay'); + overlay.style.display = 'flex'; + document.getElementById('qa-confirm-ok').onclick = function() { + closeQAConfirm(); + runQuickAction(actionKey); + }; +} + +function closeQAConfirm() { + var overlay = document.getElementById('qa-confirm-overlay'); + if (overlay) overlay.style.display = 'none'; + _qaCurrentAction = null; +} + +async function runQuickAction(actionKey) { + var banner = document.getElementById('qa-result-banner'); + if (banner) { + banner.style.display = 'block'; + banner.style.background = 'rgba(96,165,250,0.1)'; + banner.style.borderColor = 'rgba(96,165,250,0.3)'; + banner.style.color = '#60a5fa'; + banner.textContent = 'Running ' + actionKey + '...'; + } + try { + var resp = await fetch('/api/actions/run', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({action: actionKey}) + }); + var data = await resp.json(); + if (banner) { + if (data.ok) { + banner.style.background = 'rgba(34,197,94,0.1)'; + banner.style.borderColor = 'rgba(34,197,94,0.3)'; + banner.style.color = '#22c55e'; + banner.textContent = '[ok] ' + (data.output || actionKey + ' completed') + ' (' + (data.duration_ms || 0) + 'ms)'; + } else { + banner.style.background = 'rgba(239,68,68,0.1)'; + banner.style.borderColor = 'rgba(239,68,68,0.3)'; + banner.style.color = '#ef4444'; + banner.textContent = '[error] ' + (data.output || data.error || 'Action failed'); + } + } + loadQAHistory(); + } catch(e) { + if (banner) { + banner.style.background = 'rgba(239,68,68,0.1)'; + banner.style.borderColor = 'rgba(239,68,68,0.3)'; + banner.style.color = '#ef4444'; + banner.textContent = '[error] ' + e.message; + } + } +} + +async function loadQAHistory() { + var el = document.getElementById('qa-history-list'); + if (!el) return; + try { + var data = await fetchJsonWithTimeout('/api/actions/history', 5000); + var actions = data.actions || []; + if (!actions.length) { + el.textContent = 'No actions run yet.'; + return; + } + var html = ''; + actions.slice().reverse().forEach(function(a) { + var color = a.ok ? '#22c55e' : '#ef4444'; + var label = a.ok ? '[ok]' : '[error]'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + html += '
' + label + '' + (a.action || '') + '' + (a.output || '').slice(0, 120) + '' + (a.duration_ms || 0) + 'ms
'; + el.innerHTML = html; + } catch(e) { + el.textContent = 'Could not load action history.'; + } +} + +// Load QA history when Actions tab is opened +var _origSwitchTab = typeof switchTab === 'function' ? switchTab : null; +// (patched below after switchTab is defined) + var _channelsRefreshTimer = null; async function loadChannelsPage() { try { @@ -21229,6 +21469,146 @@ def api_heartbeat_ping(): return jsonify({'ok': True}) +@bp_health.route('/api/actions/run', methods=['POST']) +def api_quick_action_run(): + """Execute a Quick Action on the local OpenClaw / ClawMetry installation. + + POST body: {"action": "restart-gateway"|"clear-cache"|"rotate-logs"|"health-check"} + Returns: {"ok": bool, "action": str, "output": str, "duration_ms": int} + """ + try: + body = request.get_json(silent=True) or {} + except Exception: + body = {} + action = str(body.get('action', '')).strip() + allowed = {'restart-gateway', 'clear-cache', 'rotate-logs', 'health-check'} + if action not in allowed: + return jsonify({'ok': False, 'error': f'Unknown action: {action}', 'output': '', 'duration_ms': 0}), 400 + + t0 = time.time() + output = '' + ok = False + + try: + if action == 'restart-gateway': + # Try openclaw / systemctl restart; fallback to SIGUSR1 on the process + cmds = [ + ['openclaw', 'gateway', 'restart'], + ['systemctl', '--user', 'restart', 'openclaw'], + ['systemctl', 'restart', 'openclaw'], + ] + for cmd in cmds: + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + if r.returncode == 0: + output = (r.stdout or r.stderr or 'Gateway restart command accepted').strip() + ok = True + break + output = (r.stderr or r.stdout or 'Command failed').strip() + except FileNotFoundError: + continue + except Exception as exc: + output = str(exc) + if not ok: + output = output or 'No supported restart mechanism found (openclaw / systemctl).' + + elif action == 'clear-cache': + # Clear ClawMetry's in-process caches and any temp files + removed = [] + cache_dirs = [ + os.path.join(os.path.expanduser('~'), '.clawmetry', 'cache'), + '/tmp/clawmetry_cache', + ] + for d in cache_dirs: + if os.path.isdir(d): + import shutil as _shutil + _shutil.rmtree(d, ignore_errors=True) + removed.append(d) + # Clear OTEL in-memory store + try: + global _otel_store + _otel_store = {'tokens': [], 'cost': [], 'runs': [], 'messages': [], 'webhooks': [], 'dequeues': []} + except Exception: + pass + ok = True + output = 'Cache cleared: ' + (', '.join(removed) if removed else 'in-process caches reset') + + elif action == 'rotate-logs': + # Send SIGUSR1 to processes named 'openclaw' / 'clawmetry', which conventionally triggers log rotation + rotated = [] + import signal as _signal + for proc_name in ('openclaw', 'clawmetry'): + try: + r = subprocess.run(['pkill', '-USR1', proc_name], capture_output=True, timeout=5) + if r.returncode == 0: + rotated.append(proc_name) + except Exception: + pass + # Also attempt logrotate if available + try: + conf_path = '/etc/logrotate.d/openclaw' + if os.path.exists(conf_path) and shutil.which('logrotate'): + subprocess.run(['logrotate', '-f', conf_path], capture_output=True, timeout=10) + rotated.append('logrotate') + except Exception: + pass + ok = True + output = ('Log rotation signal sent to: ' + ', '.join(rotated)) if rotated else 'No running openclaw processes found to signal' + + elif action == 'health-check': + # Collect a quick health snapshot + health_parts = [] + cfg = _load_gw_config() + gw_url = cfg.get('url', 'http://127.0.0.1:18789') + token = cfg.get('token', '') + # Gateway ping + try: + req = _urllib_req.Request( + f"{gw_url.rstrip('/')}/api/overview", + headers={'Authorization': f'Bearer {token}'} if token else {}, + ) + with _urllib_req.urlopen(req, timeout=5) as resp: + health_parts.append(f'Gateway: OK ({resp.status})') + except Exception as exc: + health_parts.append(f'Gateway: UNREACHABLE ({exc})') + # Disk + try: + usage = shutil.disk_usage(os.path.expanduser('~')) + pct = 100 * usage.used // usage.total + health_parts.append(f'Disk: {pct}% used ({usage.free // 1_073_741_824}GB free)') + except Exception: + pass + # Session count + try: + sc = len(_get_sessions()) + health_parts.append(f'Sessions: {sc}') + except Exception: + pass + ok = True + output = ' | '.join(health_parts) or 'Health check complete' + + except Exception as exc: + output = f'Error: {exc}' + ok = False + + duration_ms = int((time.time() - t0) * 1000) + return jsonify({'ok': ok, 'action': action, 'output': output, 'duration_ms': duration_ms}) + + +@bp_health.route('/api/actions/history') +def api_quick_action_history(): + """Return in-memory log of recent quick actions (last 50 entries).""" + return jsonify({'actions': list(_quick_action_log), 'total': len(_quick_action_log)}) + + +# In-process ring-buffer for quick action history (last 50) +try: + from collections import deque as _deque + _quick_action_log = _deque(maxlen=50) +except Exception: + _quick_action_log = [] + + @bp_health.route('/api/health-stream') def api_health_stream(): """SSE endpoint - auto-refresh health checks every 30 seconds.""" diff --git a/tests/test_api.py b/tests/test_api.py index 0bc61e0..a7fa58f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -471,3 +471,60 @@ def test_memory_analytics_files_have_status(self, api, base_url): for f in d["files"]: assert_keys(f, "path", "sizeBytes", "sizeKB", "estTokens", "status") assert f["status"] in ("ok", "warning", "critical") + + +# --------------------------------------------------------------------------- +# Quick Actions (GH #252) +# --------------------------------------------------------------------------- + +class TestQuickActions: + """Tests for the Quick Actions panel API (GH #252).""" + + def test_actions_run_requires_post(self, api, base_url): + """GET /api/actions/run returns 405 (POST only).""" + r = api.get(f"{base_url}/api/actions/run", timeout=5) + assert r.status_code == 405, f"Expected 405, got {r.status_code}" + + def test_actions_run_rejects_unknown_action(self, api, base_url): + """POST with unknown action returns 400.""" + r = api.post( + f"{base_url}/api/actions/run", + json={"action": "nuke-everything"}, + timeout=10, + ) + assert r.status_code == 400, f"Expected 400, got {r.status_code}" + d = r.json() + assert d.get("ok") is False + + def test_actions_run_health_check_returns_ok(self, api, base_url): + """health-check action returns ok=True with output.""" + r = api.post( + f"{base_url}/api/actions/run", + json={"action": "health-check"}, + timeout=15, + ) + assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text[:200]}" + d = r.json() + assert_keys(d, "ok", "action", "output", "duration_ms") + assert d["action"] == "health-check" + assert isinstance(d["output"], str) + assert isinstance(d["duration_ms"], int) + + def test_actions_run_clear_cache_returns_ok(self, api, base_url): + """clear-cache action returns ok=True.""" + r = api.post( + f"{base_url}/api/actions/run", + json={"action": "clear-cache"}, + timeout=10, + ) + assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text[:200]}" + d = r.json() + assert d.get("ok") is True + assert "cache" in d.get("output", "").lower() + + def test_actions_history_structure(self, api, base_url): + """GET /api/actions/history returns list with required fields.""" + d = assert_ok(get(api, base_url, "/api/actions/history")) + assert_keys(d, "actions", "total") + assert isinstance(d["actions"], list) + assert isinstance(d["total"], int)