< Back to all hacks

#48 Dashboard via hijack.js (HTTP Intercept)

Gateway
Problem
No way to see phone status at a glance without SSH.
Solution
Inject /dashboard and /api/status into OpenClaw's HTTP server by monkey-patching http.Server.prototype.listen.
Lesson
Monkey-patching http.Server.prototype.listen lets you intercept ALL HTTP traffic in any Node.js app.

Context

PocketClaw runs headless — no screen, no keyboard, just a phone connected to WiFi. The only way to check if the gateway was running, if WiFi was up, or how much RAM was left was to SSH in and run commands manually. For a device that should run autonomously, this is unacceptable.

The solution was to inject custom HTTP routes into OpenClaw's existing web server. Since OpenClaw's source code shouldn't be modified (it's npm-installed and would be overwritten on updates), the routes are injected via hijack.js by monkey-patching Node's http.Server.prototype.listen.

Implementation

The HTTP intercept works by patching the listen method to wrap the request handler:

// In hijack.js — intercept http.Server.prototype.listen
const _origListen = require('http').Server.prototype.listen;
require('http').Server.prototype.listen = function() {
  const server = this;
  const _origEmit = server.emit;
  server.emit = function(event, req, res) {
    if (event === 'request') {
      // Intercept custom routes before OpenClaw handles them
      if (req.url === '/dashboard' || req.url === '/dashboard/') {
        return serveDashboard(req, res);
      }
      if (req.url === '/api/status') {
        return serveStatus(req, res);
      }
    }
    return _origEmit.apply(this, arguments);
  };
  return _origListen.apply(this, arguments);
};

The routes serve data from /proc filesystem:

function serveStatus(req, res) {
  const meminfo = require('fs').readFileSync('/proc/meminfo', 'utf8');
  const uptime = require('fs').readFileSync('/proc/uptime', 'utf8');

  const data = {
    ram: parseMeminfo(meminfo),
    uptime: parseFloat(uptime.split(' ')[0]),
    wifi: checkWifi(),
    gateway: 'running',
    timestamp: Date.now()
  };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

function serveDashboard(req, res) {
  // Serve inline HTML with CRT green theme
  // Auto-refreshes every 3 seconds via fetch('/api/status')
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(dashboardHTML);
}

Verification

# Test status API:
curl -s http://localhost:9000/api/status | python -m json.tool
# Expected: JSON with ram, uptime, wifi, gateway fields

# Test dashboard HTML:
curl -s http://localhost:9000/dashboard | head -5
# Expected: <!DOCTYPE html> with CRT styling

# Verify OpenClaw's own routes still work:
curl -s http://localhost:9000/api/v1/health
# Expected: OpenClaw health response (not intercepted)

Gotchas

  • The intercept must check req.url BEFORE passing to OpenClaw. If OpenClaw has a conflicting route, the intercept wins
  • The monkey-patch only catches servers created AFTER hijack.js loads. Since hijack.js loads via -r (before everything), all servers are caught
  • Dashboard HTML is inlined in hijack.js as a template string — no external files needed
  • WiFi check uses an HTTP request to clients3.google.com/generate_204 — the same endpoint Android uses for captive portal detection
  • This approach was later extended to 3 pages (Hack #49) and key management (Hack #50)

Result

MetricBeforeAfter
Status visibilitySSH onlyBrowser (any device)
Status check time30s (SSH + commands)Instant (refresh)
Extra processesN/A0 (same Node.js process)
Extra dependenciesN/A0 (built into hijack.js)