Skip to content
Open

Fix #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/gate/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,14 @@ export interface ProtocolContext {
runtime: ChatRuntime;
/** Broadcast a frame to all connected WebSocket clients. */
broadcast: (frame: Record<string, unknown>) => void;
/** Returns the current number of connected WebSocket clients. */
getClientCount: () => number;
/** The TCP port the gate server is listening on. */
serverPort: number;
}

const SERVER_START_TIME = Date.now();

// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1194,6 +1200,20 @@ async function handleRequest(ctx: ProtocolContext, req: RequestFrameT): Promise<
return ok(req.id, { messages: whatsappMessages.slice(0, limit) });
}

// ── gate ──────────────────────────────────────────────────────────────────
case "gate.status": {
const uptimeSec = (Date.now() - SERVER_START_TIME) / 1000;
return ok(req.id, {
uptime: uptimeSec,
port: ctx.serverPort,
connectedClients: ctx.getClientCount(),
pid: process.pid,
nodeVersion: process.version,
platform: process.platform,
startedAt: SERVER_START_TIME,
});
}

// ── legacy ────────────────────────────────────────────────────────────────
case "agent.echo": {
const input = requireStr(req.id, p.input, "input");
Expand Down
6 changes: 5 additions & 1 deletion src/gate/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export async function startJclawGate(options: JclawGateOptions) {
}

wss.on("connection", (socket) => {
handleWsConnection({ socket, sessions, plugins, runtime, broadcast });
handleWsConnection({
socket, sessions, plugins, runtime, broadcast,
getClientCount: () => wss.clients.size,
serverPort: options.port,
});
});

app.get("/health", (_req, res) => {
Expand Down
6 changes: 6 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"installCommand": "npm install --prefix web",
"buildCommand": "cd web && npm run build",
"outputDirectory": "web/dist"
}
Comment on lines +1 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify backend server exists and requires runtime execution

# Check for backend server entry point
echo "=== Backend server files ==="
fd -e ts -e js "server" src/gate/

# Check if WebSocket server is imported/used
echo -e "\n=== WebSocket server usage ==="
rg -n "WebSocket|createServer|listen" src/gate/server.ts

# Check package.json for backend dependencies
echo -e "\n=== Server runtime dependencies ==="
rg -A 5 "ws|express|fastify" package.json

Repository: Jacobcdsmith/jclaw-framework

Length of output: 1004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== vercel.json (root) ==="
ls -la vercel.json || true
cat -n vercel.json || true

echo -e "\n=== Any other vercel.json files ==="
fd -H -e json "vercel.json" . || true

echo -e "\n=== Root package.json ==="
ls -la package.json || true
cat -n package.json || true

echo -e "\n=== Backend startup script (if any) ==="
rg -n "\"start\"|\"dev\"|\"build\"|tsx|ts-node" package.json **/package.json || true

echo -e "\n=== Frontend websocket usage ==="
fd -e ts -e tsx "ws" web/src || true
rg -n "new WebSocket|WebSocket\\(|ws://|wss://" web/src || true

echo -e "\n=== Any Vercel/serverless functions configured ==="
rg -n "api/|functions/|serverless|vercel|`@vercel`|outputDirectory|routes" . || true

Repository: Jacobcdsmith/jclaw-framework

Length of output: 2231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo -e "\n=== web/src/ws.ts (relevant sections) ==="
nl -ba web/src/ws.ts | sed -n '1,220p'

echo -e "\n=== Find WebSocket URL construction / env vars ==="
rg -n "getWsUrl|WS_URL|websocket|wss://|ws://|VITE_|process\\.env" web/src || true

echo -e "\n=== src/gate/server.ts (startup + routes) ==="
nl -ba src/gate/server.ts | sed -n '1,260p'

echo -e "\n=== Search for gateway status RPC usage (frontend) ==="
rg -n "gate\\.status|status\\b" web/src src || true

echo -e "\n=== deployment notes (if any) ==="
ls -la
fd -e md -a . && true
rg -n "vercel|deployment|ws url|getWsUrl|gate" README* **/*.md . || true

Repository: Jacobcdsmith/jclaw-framework

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo -e "\n=== web/src/ws.ts (relevant sections) ==="
wc -l web/src/ws.ts || true
cat -n web/src/ws.ts | sed -n '1,220p'

echo -e "\n=== Find WebSocket URL construction / env vars ==="
rg -n "getWsUrl|WS_URL|websocket|wss://|ws://|VITE_|process\\.env" web/src || true

echo -e "\n=== src/gate/server.ts (startup + routes) ==="
wc -l src/gate/server.ts || true
cat -n src/gate/server.ts | sed -n '1,260p'

echo -e "\n=== Search for gateway status RPC usage (frontend) ==="
rg -n "gate\\.status|\\bstatus\\b" web/src src/gate/protocol.ts src/gate/server.ts || true

echo -e "\n=== deployment notes (if any) ==="
fd -e md -a . || true
rg -n "vercel|deployment|ws url|getWsUrl|gate" README* **/*.md . || true

Repository: Jacobcdsmith/jclaw-framework

Length of output: 26546


Critical: Vercel deploys only the dashboard; the required gate (Express+WebSocket) server isn’t deployed.

vercel.json builds/deploys only web/dist, but the app’s backend is src/gate/server.ts (Express + WebSocketServer, plus JSON-RPC methods like gate.status). The frontend computes the WebSocket URL from window.location.host, so on Vercel it will try to connect WS to the Vercel-hosted origin—there will be no running gate server, causing WS/RPC features (including gate.status) to fail.

💡 Recommended deployment approaches

Option 1: Deploy backend separately

  • Run src/gate/server.ts on a platform that supports long-running Node processes
  • Point the frontend WS URL to that backend host (instead of relying on same-origin)

Option 2: Deploy backend alongside the dashboard

  • Use a full-stack hosting option that runs the Node gate server and serves the dashboard (instead of only static web/dist)

Option 3: Convert to serverless

  • Refactor the WS server to a Vercel-compatible serverless/websocket approach (only if architecture changes are acceptable)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vercel.json` around lines 1 - 6, The current vercel.json only builds the
static dashboard, leaving the Express+WebSocket gate (src/gate/server.ts, which
creates a WebSocketServer and exposes JSON-RPC methods like gate.status)
un-deployed; fix by deploying the backend and wiring the frontend to it: either
(A) deploy src/gate/server.ts as a long-running Node service on a separate host
and change the frontend to use a configured WS URL (do not derive from
window.location.host), or (B) move to a full-stack host that runs the gate
process alongside the dashboard, or (C) refactor the gate into a
Vercel-compatible serverless/websocket solution—ensure the frontend reads the
backend host from a runtime config/env var (instead of window.location.host) so
the WebSocket connects to the running gate and gate.status works.

5 changes: 5 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SessionDetail from "./pages/SessionDetail.tsx";
import Prompts from "./pages/Prompts.tsx";
import Templates from "./pages/Templates.tsx";
import Providers from "./pages/Providers.tsx";
import Gateway from "./pages/Gateway.tsx";
import Search from "./pages/Search.tsx";
import Chat from "./pages/Chat.tsx";
import Terminal from "./pages/Terminal.tsx";
Expand Down Expand Up @@ -102,6 +103,9 @@ export default function App() {
</NavLink>

<div className="sidebar-section-label" style={{ marginTop: "8px" }}>config</div>
<NavLink to="/gateway" className={({ isActive }) => "nav-item" + (isActive ? " active" : "")} onClick={closeSidebar}>
<span className="nav-icon">⬡</span>Gateway
</NavLink>
<NavLink to="/providers" className={({ isActive }) => "nav-item" + (isActive ? " active" : "")} onClick={closeSidebar}>
<span className="nav-icon">⚙</span>Providers
</NavLink>
Expand Down Expand Up @@ -139,6 +143,7 @@ export default function App() {
<Route path="/terminal" element={<Terminal />} />
<Route path="/activity" element={<Activity />} />
<Route path="/providers" element={<Providers />} />
<Route path="/gateway" element={<Gateway />} />
<Route path="/sandbox" element={<Sandbox />} />
<Route path="/mcp" element={<Mcp />} />
<Route path="/search" element={<Search />} />
Expand Down
229 changes: 229 additions & 0 deletions web/src/pages/Gateway.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { useEffect, useState, useCallback } from "react";
import { call, onStatus, getStatus } from "../ws.ts";

interface GateStatus {
uptime: number;
port: number;
connectedClients: number;
pid: number;
nodeVersion: string;
platform: string;
startedAt: number;
}

interface PingResult {
name: string;
displayName?: string;
ok: boolean;
latencyMs: number | null;
error?: string;
}

function fmtUptime(sec: number): string {
const s = Math.floor(sec);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const parts: string[] = [];
if (d > 0) parts.push(`${d}d`);
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
parts.push(`${s % 60}s`);
return parts.join(" ");
}

function fmtTs(ms: number): string {
return new Date(ms).toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "medium",
});
}

function StatCard({ label, value, sub, accent }: {
label: string; value: string; sub?: string; accent?: string;
}) {
const color = accent ?? "var(--accent)";
return (
<div style={{
background: "var(--surface)", border: "1px solid var(--border)",
padding: "16px 20px", minWidth: 0,
}}>
<div style={{ fontSize: "10px", letterSpacing: "0.14em", color: "var(--muted)", textTransform: "uppercase", marginBottom: "8px" }}>
{label}
</div>
<div style={{ fontSize: "22px", fontWeight: 700, letterSpacing: "0.04em", color, lineHeight: 1 }}>
{value}
</div>
{sub && (
<div style={{ fontSize: "11px", color: "var(--text3)", marginTop: "6px", letterSpacing: "0.05em" }}>
{sub}
</div>
)}
</div>
);
}

export default function Gateway() {
const [status, setStatus] = useState<GateStatus | null>(null);
const [pings, setPings] = useState<PingResult[]>([]);
const [loading, setLoading] = useState(true);
const [pinging, setPinging] = useState(false);
const [error, setError] = useState<string | null>(null);
const [connected, setConnected] = useState(getStatus());
const [lastRefresh, setLastRefresh] = useState<number | null>(null);
const [pingMs, setPingMs] = useState<number | null>(null);

const fetchStatus = useCallback(async () => {
try {
const [s, p] = await Promise.all([
call<GateStatus>("gate.status"),
call<{ providers: PingResult[] }>("providers.ping").then((r) => r.providers),
]);
setStatus(s);
setPings(p);
setLastRefresh(Date.now());
setError(null);
} catch (e: unknown) {
setError(String(e));
} finally {
Comment on lines +86 to +88
setLoading(false);
}
}, []);

useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 10_000);
const unsub = onStatus((s) => {
setConnected(s);
if (s) fetchStatus();
});
return () => {
clearInterval(interval);
unsub();
};
}, [fetchStatus]);
Comment on lines +93 to +104

async function doPing() {
setPinging(true);
const t0 = Date.now();
try {
await call("ping");
setPingMs(Date.now() - t0);
} catch {
setPingMs(null);
} finally {
setPinging(false);
}
}

if (loading) return <div className="loading">Loading gateway status...</div>;

return (
<div>
<div className="page-title">Gateway</div>

{/* Connection banner */}
<div style={{
display: "flex", alignItems: "center", gap: "10px",
padding: "10px 16px", marginBottom: "24px",
background: connected ? "rgba(0,255,136,0.06)" : "rgba(255,76,76,0.06)",
border: `1px solid ${connected ? "var(--green)" : "var(--red)"}`,
}}>
<span style={{
width: "8px", height: "8px", borderRadius: "50%", flexShrink: 0,
background: connected ? "var(--green)" : "var(--red)",
boxShadow: connected ? "0 0 8px var(--green)" : "none",
}} />
<span style={{ fontSize: "12px", letterSpacing: "0.08em", color: connected ? "var(--green)" : "var(--red)" }}>
{connected ? "GATE CONNECTED" : "GATE DISCONNECTED — reconnecting..."}
</span>
{lastRefresh && (
<span style={{ fontSize: "10px", color: "var(--text3)", marginLeft: "auto" }}>
last refresh {new Date(lastRefresh).toLocaleTimeString()}
</span>
)}
</div>

{error && <div className="error-state">{error}</div>}

{/* Stat cards */}
{status && (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: "12px", marginBottom: "28px" }}>
<StatCard label="Uptime" value={fmtUptime(status.uptime)} />
<StatCard label="Port" value={String(status.port)} sub="HTTP + WS" accent="var(--accent2)" />
<StatCard
label="Clients"
value={String(status.connectedClients)}
sub="connected now"
accent={status.connectedClients > 0 ? "var(--green)" : "var(--muted)"}
/>
<StatCard label="PID" value={String(status.pid)} sub={status.platform} />
<StatCard label="Node" value={status.nodeVersion} sub="runtime" accent="var(--text2)" />
<StatCard label="Started" value={fmtTs(status.startedAt).split(",")[1]?.trim() ?? ""} sub={fmtTs(status.startedAt).split(",")[0]} accent="var(--text2)" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix incorrect date/time splitting and avoid redundant formatting calls.

The comma-split logic produces incorrect output. For a locale-formatted timestamp like "Jun 8, 2026, 10:30:00 AM", splitting by comma yields ["Jun 8", " 2026", " 10:30:00 AM"], so [1] extracts only the year ("2026") as the main value and [0] extracts the partial date ("Jun 8") as the sub-text. Additionally, fmtTs(status.startedAt) is called twice, which is inefficient.

🔧 Proposed fix to correctly split date and time
-          <StatCard label="Started" value={fmtTs(status.startedAt).split(",")[1]?.trim() ?? ""} sub={fmtTs(status.startedAt).split(",")[0]} accent="var(--text2)" />
+          <StatCard
+            label="Started"
+            value={new Date(status.startedAt).toLocaleTimeString(undefined, { timeStyle: "medium" })}
+            sub={new Date(status.startedAt).toLocaleDateString(undefined, { dateStyle: "medium" })}
+            accent="var(--text2)"
+          />

This approach formats date and time separately, avoiding fragile string splitting and redundant calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/Gateway.tsx` at line 162, The StatCard is using fragile
comma-splitting and calls fmtTs(status.startedAt) twice; instead call
fmtTs(status.startedAt) once (or use a Date/Intl.DateTimeFormat on
status.startedAt) to produce separate locale-safe date and time strings, then
pass the time string as value and the date string as sub to StatCard (refer to
fmtTs and the StatCard usage for "Started" and status.startedAt), avoiding
redundant formatting and incorrect comma-split logic.

</div>
)}

{/* Quick controls */}
<div style={{
fontSize: "11px", letterSpacing: "0.12em", color: "var(--muted)",
textTransform: "uppercase", marginBottom: "12px",
}}>
Quick Controls
</div>
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap", marginBottom: "32px" }}>
<button className="trek-btn primary" onClick={() => fetchStatus()} disabled={!connected}>
↻ refresh status
</button>
<button className="trek-btn" onClick={doPing} disabled={pinging || !connected}>
{pinging ? "pinging..." : "ping gate"}
</button>
{pingMs !== null && (
<span style={{
fontSize: "11px", letterSpacing: "0.08em",
color: pingMs < 50 ? "var(--green)" : pingMs < 200 ? "var(--accent)" : "var(--red)",
alignSelf: "center",
}}>
{pingMs}ms round-trip
</span>
)}
</div>

{/* Provider health */}
<div style={{
fontSize: "11px", letterSpacing: "0.12em", color: "var(--muted)",
textTransform: "uppercase", marginBottom: "12px",
}}>
Provider Health
</div>
{pings.length === 0 ? (
<div style={{ color: "var(--text3)", fontSize: "12px" }}>No provider results yet.</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "32px" }}>
{pings.map((p) => (
<div key={p.name} style={{
display: "flex", alignItems: "center", gap: "12px",
background: "var(--surface)", border: "1px solid var(--border)",
padding: "10px 16px",
}}>
<span style={{
width: "7px", height: "7px", borderRadius: "50%", flexShrink: 0,
background: p.ok ? "var(--green)" : "var(--red)",
}} />
<span style={{ fontSize: "12px", letterSpacing: "0.06em", color: "var(--fg)", flex: 1, minWidth: 0 }}>
{(p.displayName ?? p.name).toUpperCase()}
</span>
<span style={{
fontSize: "11px", letterSpacing: "0.05em",
color: p.ok ? "var(--green)" : "var(--muted)",
}}>
{p.ok
? `online — ${p.latencyMs ?? "N/A"}ms`
: `offline${p.error ? ` — ${p.error}` : ""}`}
</span>
</div>
))}
</div>
)}
</div>
);
}
22 changes: 17 additions & 5 deletions web/src/pages/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { call, onEvent } from "../ws.ts";
import { useEffect, useState, useCallback } from "react";
import { call, onEvent, onStatus } from "../ws.ts";
import { Link } from "react-router-dom";

interface Stats {
Expand Down Expand Up @@ -121,7 +121,10 @@ export default function Overview() {
const [recentTokens, setRecentTokens] = useState<{ ts: number }[]>([]);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchData = useCallback(() => {
// Clear any prior error so the UI reflects the freshest attempt.
// Individual call failures below will set a new error if they occur.
setError(null);
call<Stats>("sessions.stats").then(setStats).catch((e: Error) => setError(e.message));
call<{ sessions: Session[] }>("sessions.list", { limit: 5 })
.then((r) => setRecentSessions(r.sessions))
Expand Down Expand Up @@ -164,6 +167,15 @@ export default function Overview() {
);
setLive((prev) => ({ ...prev, totalOutputToday: todayTotal }));
}).catch(() => {});
}, []);

useEffect(() => {
fetchData();

// Re-fetch whenever the gate reconnects
const offStatus = onStatus((connected) => {
if (connected) fetchData();
});

// Real-time token stream tracking
const offEvent = onEvent((event, payload) => {
Expand Down Expand Up @@ -195,8 +207,8 @@ export default function Overview() {
setRecentTokens((prev) => prev.filter((t) => now - t.ts < 5_000));
}, 1000);

return () => { offEvent(); clearInterval(ticker); };
}, []);
return () => { offStatus(); offEvent(); clearInterval(ticker); };
}, [fetchData]);

const totalTokens = stats ? stats.totalInputTokens + stats.totalOutputTokens : 0;
const onlineCount = providers.filter((p) => p.ok).length;
Expand Down
6 changes: 6 additions & 0 deletions web/src/pages/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const ALIASES: Record<string, { method: string; params?: Record<string, unknown>
"prompts.list": { method: "prompts.list" },
templates: { method: "templates.list" },
"templates.list": { method: "templates.list" },
gateway: { method: "gate.status" },
"gate.status": { method: "gate.status" },
"gate.info": { method: "gate.status" },
};

const HELP_TEXT = `JCLAW TERMINAL — available commands:
Expand All @@ -37,6 +40,9 @@ const HELP_TEXT = `JCLAW TERMINAL — available commands:
stats aggregate session stats
prompts list saved prompts
templates list saved templates
gateway gate server status (uptime, port, clients)
gate.status gate server status (uptime, port, clients)
gate.info gate server info (alias for gate.status)
clear clear this terminal
help show this message

Expand Down
Loading