< Back to all hacks

#49 3-Page Dashboard (STATUS / KEYS / LOGS)

Gateway
Problem
Single dashboard page insufficient as PocketClaw grew. No way to view logs or manage keys without SSH.
Solution
Extended hijack.js HTTP interceptor to serve 3 pages with tab navigation and CRT theme.
Lesson
A single hijack.js file can serve an entire web application alongside the main gateway.

Context

As PocketClaw matured, a single status page wasn't enough. Changing an API key required SSH access and manual file editing. Reading gateway logs meant tailing files over SSH. For a device meant to run autonomously, all management should be accessible from a web browser.

The dashboard was extended from 1 page to 3 pages, plus 4 API endpoints, all served from hijack.js within the same Node.js process.

Implementation

Routes added to the HTTP interceptor:

RoutePageFunction
/dashboardSTATUSServices, RAM bar, swap, top processes, uptime
/keysKEYSView masked keys, edit, test connectivity
/logsLOGSReal-time gateway logs, color-coded errors
/api/statusJSONStatus data
/api/keysJSONKey list (GET) / save (POST)
/api/keys/testJSONTest key validity
/api/logsJSONLog buffer + lazy load history

All pages share a common CRT-themed layout with tab navigation:

function getTabNav(active) {
  return `
    <nav style="display:flex;gap:8px;margin-bottom:24px">
      <a href="/dashboard" ${active==='status'?'class="active"':''}>STATUS</a>
      <a href="/keys" ${active==='keys'?'class="active"':''}>KEYS</a>
      <a href="/logs" ${active==='logs'?'class="active"':''}>LOGS</a>
    </nav>
  `;
}

All pages auto-refresh every 2-3 seconds via fetch() to their respective API endpoints:

// Auto-refresh pattern used by all 3 pages:
setInterval(async () => {
  const res = await fetch('/api/status');
  const data = await res.json();
  updateDOM(data);
}, 3000);

Verification

# Test all 3 pages:
curl -s http://localhost:9000/dashboard | grep "STATUS"
# Expected: STATUS tab active

curl -s http://localhost:9000/keys | grep "KEYS"
# Expected: KEYS tab active

curl -s http://localhost:9000/logs | grep "LOGS"
# Expected: LOGS tab active

# Test API endpoints:
curl -s http://localhost:9000/api/status | head -1
curl -s http://localhost:9000/api/keys | head -1
curl -s http://localhost:9000/api/logs | head -1
# Expected: JSON responses for each

Gotchas

  • All HTML is inlined in hijack.js — no external files, no build step, no static file serving
  • The log buffer keeps the last 1000 lines in memory. Older logs are read from the log file on demand (lazy loading)
  • Key values are masked in the GET response (only last 4 chars shown). Full values are only accessible when editing
  • The /api/keys POST endpoint writes directly to the env file and updates process.env without gateway restart
  • Auto-refresh at 2-3 second intervals creates ~20-30 requests/minute. On a single-core ARM32, this is noticeable but acceptable

Result

MetricBeforeAfter
Dashboard pages13
API endpoints14
SSH required forKeys, logsNothing (web UI)
Management overheadManualBrowser-based