AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "openclaw-dashboard",
"installedVersion": "1.0.9",
"installedAt": 1772222830957
}

View File

@@ -0,0 +1,100 @@
# OpenClaw Dashboard (Public)
Mobile-first operations dashboard for OpenClaw, focused on sessions, costs, cron, watchdog, and day-to-day operations.
This public repository is sanitized and simplified for sharing.
## Install via ClawHub
```bash
clawhub install openclaw-dashboard
cd ~/.openclaw/workspace/skills/openclaw-dashboard
cp env.example .env
node api-server.js
```
Then open http://localhost:18791
## Quick Start (from source)
```bash
git clone https://github.com/JonathanJing/openclaw-dashboard.git
cd openclaw-dashboard
cp env.example .env
# edit .env with your own values
node api-server.js
```
Then open:
- `http://localhost:18791/`
## Required Configuration
Default install has no hard key requirement.
- `OPENCLAW_AUTH_TOKEN` is **optional but recommended** for protected/local-auth usage.
- `gateway.authToken` is treated as optional capability context in skill metadata.
Use `env.example` (ClawHub package) or `.env.example` (source checkout) for optional overrides.
## Compliance Defaults (Important)
This public package now ships with restricted defaults:
- Dashboard binds to localhost by default (`DASHBOARD_HOST=127.0.0.1`)
- No automatic loading of `~/.openclaw/keys.env` unless `OPENCLAW_LOAD_KEYS_ENV=1`
- Provider org audit endpoint disabled unless `OPENCLAW_ENABLE_PROVIDER_AUDIT=1`
- Config file view endpoint (`/ops/config`) disabled unless `OPENCLAW_ENABLE_CONFIG_ENDPOINT=1`
- Absolute-path attachment copy mode disabled unless `OPENCLAW_ALLOW_ATTACHMENT_FILEPATH_COPY=1`
- Even when enabled, attachment copy only allows repo-local paths by default
- Extra source paths require explicit flags: `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_TMP=1`, `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_WORKSPACE=1`, and/or `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_OPENCLAW_HOME=1`
- User-scoped systemctl restart disabled unless `OPENCLAW_ENABLE_SYSTEMCTL_RESTART=1`
- Frontend no longer sends auth token in query parameters for API calls
- Cron/task text sent to hooks is sanitized and treated as untrusted payload
- Mutating operations are disabled unless `OPENCLAW_ENABLE_MUTATING_OPS=1` and request is from localhost
- `server-monitor.html` now uses integrated authenticated `/metrics` endpoint
These defaults reduce accidental secret ingestion and over-broad local file access.
## Core Files
- `api-server.js`: backend API and operations logic
- `agent-dashboard.html`: single-file frontend UI
- `SKILL.md`: repository-level agent instructions
## Security Notes
- No real tokens should be committed.
- Keep secrets in local environment files only.
- Rotate tokens immediately if exposure is suspected.
## VirusTotal Compliance
Run a pre-release hash and upload workflow before publishing:
```bash
shasum -a 256 api-server.js agent-dashboard.html SKILL.md README.md env.example > vt-hashes.txt
```
Then submit these hashes/files to VirusTotal and attach the report IDs to your release notes.
If any file is flagged, block release and investigate before publishing.
## Publish to ClawHub
This repository is prepared as a ClawHub skill package with root-level `SKILL.md`.
```bash
clawhub publish . \
--slug openclaw-dashboard \
--name "OpenClaw Dashboard" \
--version 1.0.9 \
--changelog "Risk-surface reduction: localhost bind default, no token-in-query API usage, tighter attachment copy defaults, and integrated /metrics endpoint."
```
If your local version changes, update both:
- `SKILL.md` frontmatter `version`
- `clawhub publish --version`
## License
MIT

View File

@@ -0,0 +1,65 @@
# Security Policy
## Threat Model (Summary)
- This dashboard reads local OpenClaw runtime data from `~/.openclaw/...`.
- Some operations can trigger local hooks and update workflows.
- Secrets and provider admin keys must remain opt-in and least-privilege.
## Secure Defaults
- `OPENCLAW_LOAD_KEYS_ENV=0` (disabled by default)
- `OPENCLAW_ENABLE_PROVIDER_AUDIT=0`
- `OPENCLAW_ENABLE_CONFIG_ENDPOINT=0`
- `OPENCLAW_ALLOW_ATTACHMENT_FILEPATH_COPY=0`
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_TMP=0`
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_WORKSPACE=0`
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_OPENCLAW_HOME=0`
- `OPENCLAW_ENABLE_SYSTEMCTL_RESTART=0`
- `OPENCLAW_ENABLE_MUTATING_OPS=0`
- `DASHBOARD_HOST=127.0.0.1`
- `DASHBOARD_CORS_ORIGINS=` (empty = loopback only, no wildcard)
Mutating operations are additionally restricted to localhost callers.
## CORS Policy
CORS is restricted by default — only loopback origins (`localhost`, `127.0.0.1`) are allowed.
To allow external origins (e.g. for Tailscale Funnel access), set:
```
DASHBOARD_CORS_ORIGINS=https://your-tailscale-hostname.ts.net
```
Multiple origins: comma-separated. Use `*` only in trusted environments.
## Command Execution
All child_process calls use `execFileSync` with argument arrays (no shell interpolation).
Zero `execSync` calls exist in the codebase. The `runCmd()` helper wraps `execFileSync` with timeout and error handling.
## File Copy (FILEPATH_COPY)
When `OPENCLAW_ALLOW_ATTACHMENT_FILEPATH_COPY=1` is set:
- Source paths must match configured allowed prefixes
- Symlinks are resolved via `realpathSync` and re-checked against allowed prefixes
- Each sub-directory (`/tmp`, workspace, `.openclaw`) requires its own explicit env flag
## VirusTotal Compliance Checklist
Before each release:
1. Generate hashes:
```bash
shasum -a 256 api-server.js agent-dashboard.html SKILL.md README.md .env.example > vt-hashes.txt
```
2. Submit hashes/files to VirusTotal.
3. Record report IDs in release notes.
4. Block release if suspicious detections are unresolved.
## Reporting a Vulnerability
Open a private security report with:
- affected version
- reproduction steps
- impact assessment
- suggested mitigation

View File

@@ -0,0 +1,174 @@
---
name: openclaw-dashboard
description: Builds and maintains the public OpenClaw dashboard repository with sanitization-first rules. Use when adding features, adjusting `api-server.js` routes, changing `agent-dashboard.html`, or preparing public-safe docs and configuration.
version: "1.0.9"
metadata:
{
"openclaw":
{
"emoji": "📊",
"requires": { "bins": ["node", "openclaw"] },
"optionalRequires":
{
"config": ["gateway.authToken"],
"env": ["OPENCLAW_AUTH_TOKEN"],
},
"optionalEnv":
[
"OPENCLAW_HOOK_TOKEN",
"OPENCLAW_LOAD_KEYS_ENV",
"OPENCLAW_KEYS_ENV_PATH",
"OPENCLAW_ENABLE_PROVIDER_AUDIT",
"OPENCLAW_ENABLE_CONFIG_ENDPOINT",
"OPENCLAW_ENABLE_SESSION_PATCH",
"OPENCLAW_ALLOW_ATTACHMENT_FILEPATH_COPY",
"OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_TMP",
"OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_WORKSPACE",
"OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_OPENCLAW_HOME",
"OPENCLAW_ENABLE_SYSTEMCTL_RESTART",
"OPENCLAW_ENABLE_MUTATING_OPS",
"NOTION_API_KEY",
"OPENAI_ADMIN_KEY",
"ANTHROPIC_ADMIN_KEY",
"VISION_DB_NETWORKING",
"VISION_DB_WINE",
"VISION_DB_CIGAR",
"VISION_DB_TEA",
],
},
}
---
# OpenClaw Dashboard
A mobile-friendly operational dashboard for OpenClaw agents.
## Quick Start (ClawHub Install)
1. Install: `clawhub install openclaw-dashboard`
2. Navigate: `cd ~/.openclaw/workspace/skills/openclaw-dashboard`
3. Copy config: `cp .env.example .env` (edit as needed)
4. Start: `node api-server.js`
5. Open: http://localhost:18791
## Configuration
| Env Variable | Default | Description |
|---|---|---|
| `OPENCLAW_AUTH_TOKEN` | (none) | Access token. If unset, open on localhost |
| `DASHBOARD_PORT` | 18791 | Server port |
| `DASHBOARD_HOST` | 127.0.0.1 | Bind address |
| `DASHBOARD_TITLE` | OpenClaw Dashboard | Browser tab title |
## Authentication
- **No token set**: Dashboard is accessible without auth on localhost
- **Token set**: Access via `http://localhost:18791/login` or append `?token=yourtoken`
## Verify It Works
```bash
curl http://localhost:18791/health
```
## Prerequisites
- Node.js 20+
- OpenClaw running on the same machine
---
## For Contributors
## Mission
Keep this repository public-safe and easy to run. Prioritize:
1. Secret sanitization
2. Minimal setup steps
3. Stable API/UI behavior
## Apply when
Use this skill for:
- Dashboard feature requests (sessions, cost, cron, watchdog, operations)
- Backend route updates in `api-server.js`
- Frontend behavior updates in `agent-dashboard.html`
- README, setup, and environment simplification
- Public release checks for accidental sensitive data
## Public-safety guardrails
- Never hardcode tokens, API keys, cookies, or host-specific secrets.
- Never commit machine-specific absolute paths.
- Prefer `process.env.*` and safe defaults based on `HOME`.
- Keep examples as placeholders (`your_token_here`, `/path/to/...`).
- If uncertain, redact first and ask the user before exposing details.
- Keep sensitive behaviors opt-in (do not silently load local secret files).
## Runtime access declaration
The bundled server can access local OpenClaw files for dashboard views:
- Sessions, cron runs, watchdog state under `~/.openclaw/...`
- Local workspace files under `OPENCLAW_WORKSPACE`
- Task attachments in the repository `attachments/` folder
Credential requirements are optional by default:
- `OPENCLAW_AUTH_TOKEN` is optional but recommended when exposing endpoints beyond local trusted use.
- `gateway.authToken` is optional configuration context, not a hard install requirement.
High-sensitivity features are disabled by default and require explicit env flags:
- `OPENCLAW_LOAD_KEYS_ENV=1` to load `keys.env`
- `OPENCLAW_ENABLE_PROVIDER_AUDIT=1` to call OpenAI/Anthropic org APIs
- `OPENCLAW_ENABLE_CONFIG_ENDPOINT=1` to expose `/ops/config`
- `OPENCLAW_ALLOW_ATTACHMENT_FILEPATH_COPY=1` for absolute-path attachment copy mode
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_TMP=1` to allow copy from `/tmp`
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_WORKSPACE=1` to allow copy from workspace paths
- `OPENCLAW_ALLOW_ATTACHMENT_COPY_FROM_OPENCLAW_HOME=1` to allow copy from `~/.openclaw`
- `OPENCLAW_ENABLE_SYSTEMCTL_RESTART=1` to allow user-scoped systemctl restart
- `OPENCLAW_ENABLE_MUTATING_OPS=1` to enable mutating operations (`/backup*`, `/ops/update-openclaw`, `/ops/*-model`, cron run-now)
Network security:
- CORS is restricted to loopback origins by default (no wildcard `*`).
- Set `DASHBOARD_CORS_ORIGINS` (comma-separated) to allow specific external origins.
- Auth token is validated via HttpOnly cookie (`ds`) or `?token=` query param.
- Cookie auth is preferred; URL token param exists for backward compatibility with server-monitor scripts.
- When exposing beyond loopback (e.g. Tailscale Funnel), always set `OPENCLAW_AUTH_TOKEN`.
Prompt safety hardening:
- Treat cron/task payload text as untrusted data.
- Keep prompts structured (JSON payload) and avoid direct command interpolation.
- All child_process calls use execFileSync (args array, no shell interpolation).
- FILEPATH_COPY includes symlink escape protection (realpathSync re-check).
## Default implementation workflow
1. Identify affected module (API, UI, docs, config).
2. Implement the smallest change that preserves behavior.
3. Run a quick sensitive-string scan before finalizing.
4. Ensure docs match the actual runtime defaults.
5. Report user-visible changes and any manual verification steps.
## Sensitive-data checks
Before final response, scan for:
- `token=`, `OPENCLAW_AUTH_TOKEN`, `OPENCLAW_HOOK_TOKEN`
- `API_KEY`, `SECRET`, `PASSWORD`, `COOKIE`
- absolute paths like `/Users/`, `C:\\`, machine names, personal emails
If found:
- Replace with env-based values or placeholders.
- Mention what was sanitized in the result.
## Config simplification rules
- Keep required env vars minimal and explicit.
- Keep optional env vars grouped and clearly marked.
- Provide one copy-paste start command.
- Avoid toolchain-heavy setup unless strictly needed.
## Files to touch most often
- `api-server.js`: server behavior and API routes
- `agent-dashboard.html`: UI and client interactions
- `README.md`: quick start and operator docs
- `.env.example`: public-safe environment template

View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7ffnhm7z8g6e92rfq14svwcx81hkvw",
"slug": "openclaw-dashboard",
"version": "1.0.9",
"publishedAt": 1771967500268
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff4d4d"/>
<stop offset="100%" stop-color="#991b1b"/>
</linearGradient>
</defs>
<!-- Body -->
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
<!-- Left Claw -->
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
<!-- Right Claw -->
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
<!-- Antenna -->
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<!-- Eyes -->
<circle cx="45" cy="35" r="6" fill="#050810"/>
<circle cx="75" cy="35" r="6" fill="#050810"/>
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180">
<rect width="180" height="180" rx="32" fill="#0d1117"/>
<text x="90" y="100" font-size="110" text-anchor="middle" dominant-baseline="central">🦞</text>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Monitor — OpenClaw</title>
<meta name="description" content="Real-time server monitoring dashboard for OpenClaw with CPU, memory, disk, and network metrics.">
<meta name="author" content="Abo-Elmakarem Shohoud">
<meta property="og:title" content="OpenClaw Server Monitor">
<meta property="og:description" content="Real-time server monitoring dashboard for OpenClaw with CPU, memory, disk, and network metrics.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://github.com/JonathanJing/openclaw-dashboard">
<meta property="og:image" content="https://raw.githubusercontent.com/JonathanJing/openclaw-dashboard/main/screenshots/tasks-kanban.png">
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#06080e;--bg2:#0a0d14;--card:#0d1117;--border:#1a1f2e;
--text:#e6edf3;--text2:#8b949e;--accent:#7c5cfc;--accent2:#a78bfa;
--green:#2dd4a0;--yellow:#f5a623;--red:#f85149;--blue:#58a6ff;
--glass:rgba(13,17,23,0.7);--glow:rgba(124,92,252,0.15);
--font:'Outfit',system-ui,sans-serif;--mono:'Space Mono',monospace;
}
body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:100vh;overflow-x:hidden}
.bg-mesh{position:fixed;inset:0;z-index:0;pointer-events:none;
background:radial-gradient(ellipse 80% 60% at 20% 20%,rgba(124,92,252,0.06),transparent),
radial-gradient(ellipse 60% 50% at 80% 80%,rgba(45,212,160,0.04),transparent),
radial-gradient(ellipse 50% 40% at 50% 50%,rgba(88,166,255,0.03),transparent)}
.container{max-width:1200px;margin:0 auto;padding:20px;position:relative;z-index:1}
header{display:flex;align-items:center;justify-content:space-between;padding:16px 0 24px;border-bottom:1px solid var(--border);margin-bottom:24px}
.logo{font-size:1.5rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-size:200% 200%;animation:shimmer 6s ease infinite}
@keyframes shimmer{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.logo-sub{font-size:.7rem;letter-spacing:3px;text-transform:uppercase;color:var(--text2);margin-top:2px}
.status-dot{width:10px;height:10px;border-radius:50%;background:var(--green);display:inline-block;animation:pulse 2s infinite;margin-right:8px}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(45,212,160,0.4)}50%{box-shadow:0 0 0 6px rgba(45,212,160,0)}}
.header-right{display:flex;align-items:center;gap:12px;font-size:.85rem;color:var(--text2)}
.back-link{color:var(--accent);text-decoration:none;font-size:.85rem;border:1px solid var(--border);padding:6px 14px;border-radius:999px;transition:all .2s}
.back-link:hover{background:var(--glow);border-color:var(--accent)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-bottom:24px}
.card{background:var(--glass);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:16px;padding:20px;position:relative;overflow:hidden;transition:all .3s;animation:cardIn .5s ease backwards}
.card:hover{transform:translateY(-2px);box-shadow:0 8px 32px rgba(124,92,252,0.1)}
.card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:3px 0 0 3px}
.card.cpu::before{background:linear-gradient(180deg,var(--accent),var(--blue))}
.card.mem::before{background:linear-gradient(180deg,var(--green),var(--blue))}
.card.disk::before{background:linear-gradient(180deg,var(--yellow),var(--accent))}
.card.net::before{background:linear-gradient(180deg,var(--blue),var(--accent2))}
.card.up::before{background:linear-gradient(180deg,var(--green),var(--yellow))}
.card.load::before{background:linear-gradient(180deg,var(--red),var(--yellow))}
@keyframes cardIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
.card:nth-child(1){animation-delay:.05s}.card:nth-child(2){animation-delay:.1s}.card:nth-child(3){animation-delay:.15s}
.card:nth-child(4){animation-delay:.2s}.card:nth-child(5){animation-delay:.25s}.card:nth-child(6){animation-delay:.3s}
.card-label{font-size:.7rem;letter-spacing:2px;text-transform:uppercase;color:var(--text2);margin-bottom:8px}
.card-value{font-size:2rem;font-weight:700;margin-bottom:4px}
.card-sub{font-size:.8rem;color:var(--text2);font-family:var(--mono)}
.progress-bar{height:6px;background:var(--border);border-radius:6px;margin-top:12px;overflow:hidden}
.progress-fill{height:100%;border-radius:6px;transition:width .6s ease}
.fill-cpu{background:linear-gradient(90deg,var(--accent),var(--blue))}
.fill-mem{background:linear-gradient(90deg,var(--green),var(--blue))}
.fill-disk{background:linear-gradient(90deg,var(--yellow),var(--accent))}
.section{margin-bottom:24px}
.section-title{font-size:1.1rem;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-title::before{content:'';width:4px;height:18px;border-radius:4px;background:linear-gradient(180deg,var(--accent),var(--blue))}
.procs-table{width:100%;border-collapse:collapse;font-size:.82rem}
.procs-table th{text-align:left;padding:10px 12px;font-size:.7rem;letter-spacing:1px;text-transform:uppercase;color:var(--text2);border-bottom:1px solid var(--border)}
.procs-table td{padding:8px 12px;border-bottom:1px solid rgba(26,31,46,0.5);font-family:var(--mono);font-size:.78rem}
.procs-table tr:hover td{background:rgba(124,92,252,0.04)}
.chart-container{background:var(--glass);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:16px;padding:20px;margin-bottom:16px}
canvas{width:100%!important;height:120px!important}
.sys-info{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px;font-size:.82rem}
.sys-info-item{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(26,31,46,0.3)}
.sys-info-key{color:var(--text2)}.sys-info-val{font-family:var(--mono);color:var(--text)}
.err{text-align:center;padding:40px;color:var(--red);font-size:1.1rem}
.loading{text-align:center;padding:40px;color:var(--text2)}
@media(max-width:600px){.grid{grid-template-columns:1fr}.card-value{font-size:1.5rem}header{flex-direction:column;gap:12px}}
</style>
</head>
<body>
<div class="bg-mesh"></div>
<div class="container">
<header>
<div>
<div class="logo">Server Monitor</div>
<div class="logo-sub">OpenClaw Infrastructure</div>
</div>
<div class="header-right">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Connecting...</span>
<a href="/" class="back-link">← Control UI</a>
</div>
</header>
<div class="grid" id="cards">
<div class="card cpu" style="animation-delay:.05s">
<div class="card-label">CPU Usage</div>
<div class="card-value" id="cpuVal"></div>
<div class="card-sub" id="cpuSub"></div>
<div class="progress-bar"><div class="progress-fill fill-cpu" id="cpuBar" style="width:0%"></div></div>
</div>
<div class="card mem" style="animation-delay:.1s">
<div class="card-label">Memory</div>
<div class="card-value" id="memVal"></div>
<div class="card-sub" id="memSub"></div>
<div class="progress-bar"><div class="progress-fill fill-mem" id="memBar" style="width:0%"></div></div>
</div>
<div class="card disk" style="animation-delay:.15s">
<div class="card-label">Disk</div>
<div class="card-value" id="diskVal"></div>
<div class="card-sub" id="diskSub"></div>
<div class="progress-bar"><div class="progress-fill fill-disk" id="diskBar" style="width:0%"></div></div>
</div>
<div class="card net" style="animation-delay:.2s">
<div class="card-label">Network</div>
<div class="card-value" id="netVal"></div>
<div class="card-sub" id="netSub"></div>
</div>
<div class="card up" style="animation-delay:.25s">
<div class="card-label">Uptime</div>
<div class="card-value" id="uptimeVal"></div>
<div class="card-sub" id="uptimeSub"></div>
</div>
<div class="card load" style="animation-delay:.3s">
<div class="card-label">Load Average</div>
<div class="card-value" id="loadVal"></div>
<div class="card-sub" id="loadSub"></div>
</div>
</div>
<div class="section">
<div class="section-title">CPU History (5 min)</div>
<div class="chart-container"><canvas id="cpuChart"></canvas></div>
</div>
<div class="section">
<div class="section-title">Memory History (5 min)</div>
<div class="chart-container"><canvas id="memChart"></canvas></div>
</div>
<div class="section">
<div class="section-title">Top Processes</div>
<div style="background:var(--glass);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:16px;overflow:hidden">
<table class="procs-table">
<thead><tr><th>Command</th><th>PID</th><th>CPU %</th><th>Mem %</th><th>User</th></tr></thead>
<tbody id="procsBody"><tr><td colspan="5" style="text-align:center;color:var(--text2)">Loading...</td></tr></tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-title">System Info</div>
<div style="background:var(--glass);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:16px;padding:20px">
<div class="sys-info" id="sysInfo"></div>
</div>
</div>
</div>
<script>
const TOKEN = new URLSearchParams(location.search).get('token') || localStorage.getItem('openclaw_token') || '';
if (TOKEN) {
localStorage.setItem('openclaw_token', TOKEN);
try {
const clean = `${location.origin}${location.pathname}${location.hash || ''}`;
history.replaceState(null, '', clean);
} catch {}
}
const API = (location.port === '18789' || location.port === '18790')
? `${location.protocol}//${location.hostname}:18790/metrics`
: `/metrics`;
let cpuHistory = [], memHistory = [];
async function fetchMetrics() {
try {
const r = await fetch(API, {
credentials: 'same-origin',
headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
update(d);
document.getElementById('statusDot').style.background = 'var(--green)';
document.getElementById('statusText').textContent = 'Live — ' + new Date(d.timestamp).toLocaleTimeString();
} catch (e) {
// Fallback: try static JSON
try {
const r2 = await fetch('/metrics.json?' + Date.now());
if (r2.ok) { const d = await r2.json(); update(d);
document.getElementById('statusDot').style.background = 'var(--yellow)';
document.getElementById('statusText').textContent = 'Static — ' + new Date(d.timestamp).toLocaleTimeString();
return;
}
} catch(e2) {}
document.getElementById('statusDot').style.background = 'var(--red)';
document.getElementById('statusText').textContent = 'Error: ' + e.message;
}
}
function update(d) {
// CPU
const cpuPct = d.cpu.overall;
document.getElementById('cpuVal').textContent = cpuPct + '%';
document.getElementById('cpuSub').textContent = `${d.cpu.count} cores — ${d.cpu.model.substring(0,35)}`;
document.getElementById('cpuBar').style.width = cpuPct + '%';
setCriticalColor('cpuBar', cpuPct, 'fill-cpu');
// Memory
document.getElementById('memVal').textContent = d.memory.pct + '%';
document.getElementById('memSub').textContent = `${d.memory.usedHuman} / ${d.memory.totalHuman}`;
document.getElementById('memBar').style.width = d.memory.pct + '%';
setCriticalColor('memBar', d.memory.pct, 'fill-mem');
// Disk
document.getElementById('diskVal').textContent = d.disk.pct + '%';
document.getElementById('diskSub').textContent = `${d.disk.usedHuman} / ${d.disk.totalHuman}`;
document.getElementById('diskBar').style.width = d.disk.pct + '%';
setCriticalColor('diskBar', d.disk.pct, 'fill-disk');
// Network
document.getElementById('netVal').innerHTML = `${d.network.rxRateHuman || '0 B/s'}`;
document.getElementById('netSub').textContent = `${d.network.txRateHuman || '0 B/s'} — Total: ${d.network.totalRxHuman || '0 B'} rx / ${d.network.totalTxHuman || '0 B'} tx`;
// Uptime
document.getElementById('uptimeVal').textContent = d.uptime.human;
document.getElementById('uptimeSub').textContent = d.hostname;
// Load
document.getElementById('loadVal').textContent = d.loadAvg['1m'];
document.getElementById('loadSub').textContent = `5m: ${d.loadAvg['5m']} — 15m: ${d.loadAvg['15m']}`;
// Processes
const tbody = document.getElementById('procsBody');
if (d.topProcesses && d.topProcesses.length) {
tbody.innerHTML = d.topProcesses.map(p => `<tr><td>${esc(p.command)}</td><td>${p.pid}</td><td>${p.cpu}</td><td>${p.mem}</td><td>${p.user}</td></tr>`).join('');
}
// System info
document.getElementById('sysInfo').innerHTML = [
['Hostname', d.hostname], ['Platform', d.platform + ' / ' + d.arch],
['Node.js', d.nodeVersion], ['CPU Model', d.cpu.model],
['Total Memory', d.memory.totalHuman], ['Disk Mount', d.disk.mount || '/'],
].map(([k,v])=>`<div class="sys-info-item"><span class="sys-info-key">${k}</span><span class="sys-info-val">${v||'—'}</span></div>`).join('');
// History
if (d.history) {
cpuHistory = d.history.cpu || [];
memHistory = d.history.mem || [];
} else {
cpuHistory.push({ ts: Date.now(), value: cpuPct });
memHistory.push({ ts: Date.now(), value: d.memory.pct });
if (cpuHistory.length > 60) cpuHistory.shift();
if (memHistory.length > 60) memHistory.shift();
}
drawChart('cpuChart', cpuHistory, 'rgba(124,92,252,0.8)', 'rgba(124,92,252,0.1)');
drawChart('memChart', memHistory, 'rgba(45,212,160,0.8)', 'rgba(45,212,160,0.1)');
}
function setCriticalColor(id, pct) {
const el = document.getElementById(id);
if (pct >= 90) el.style.background = 'linear-gradient(90deg,var(--red),var(--yellow))';
else if (pct >= 75) el.style.background = 'linear-gradient(90deg,var(--yellow),var(--accent))';
}
function drawChart(canvasId, data, stroke, fill) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const w = rect.width, h = rect.height;
ctx.clearRect(0, 0, w, h);
if (!data.length) return;
// Grid lines
ctx.strokeStyle = 'rgba(26,31,46,0.5)';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = (h / 4) * i;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
}
// Data
const vals = data.map(d => d.value);
const max = 100;
ctx.beginPath();
for (let i = 0; i < vals.length; i++) {
const x = (i / Math.max(vals.length - 1, 1)) * w;
const y = h - (vals[i] / max) * h;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.strokeStyle = stroke;
ctx.lineWidth = 2;
ctx.stroke();
// Fill
ctx.lineTo(w, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
// Current value label
const last = vals[vals.length - 1];
ctx.fillStyle = stroke;
ctx.font = '600 12px Outfit, sans-serif';
ctx.fillText(last + '%', w - 40, 16);
}
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
// Initial fetch + interval
fetchMetrics();
setInterval(fetchMetrics, 5000);
</script>
<footer style="text-align:center;padding:24px 0 16px;color:#8b949e;font-size:0.85rem;font-family:'Outfit',sans-serif;">
Built for OpenClaw Dashboard ·
<a href="https://github.com/JonathanJing/openclaw-dashboard" target="_blank" style="color:#8b949e;text-decoration:none;">GitHub</a>
</footer>
</body>
</html>