Skip to content

feat: WhatsApp channel integration + framework status dashboard with live processing metrics#7

Merged
Jacobcdsmith merged 3 commits into
mainfrom
copilot/add-ui-elements-for-frameworks
May 2, 2026
Merged

feat: WhatsApp channel integration + framework status dashboard with live processing metrics#7
Jacobcdsmith merged 3 commits into
mainfrom
copilot/add-ui-elements-for-frameworks

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 11, 2026

Adds Meta WhatsApp Business Cloud API as a first-class channel and surfaces real-time framework state across the dashboard — both previously absent from the UI and backend.

Backend

  • src/channels/plugins/whatsapp.ts — outbound adapter calling POST /v20.0/{phoneNumberId}/messages; guards globalThis.fetch availability at call-time and returns a descriptive error when unavailable (Node 18+ required)
  • src/gate/whatsapp-store.ts — shared in-process store (max 500) for inbound + outbound messages; cap enforced inside the module via a pushWhatsAppMessage() helper used by all callers
  • src/gate/server.ts — two new HTTP routes:
    • GET /webhook/whatsapp — Meta hub verification; hub.challenge sanitized to /^\d+$/ to prevent reflected XSS
    • POST /webhook/whatsapp — validates X-Hub-Signature-256 HMAC-SHA256 signature using timingSafeEqual when appSecret is configured; skips messages with empty id or from; broadcasts whatsapp.message WS event to all connected clients via a shared broadcast helper
  • src/gate/protocol.ts — four new RPC methods: whatsapp.config.get (masks accessToken, verifyToken, and appSecret), whatsapp.config.set, whatsapp.send (broadcasts outbound events to all WS clients), whatsapp.messages.list; ProtocolContext extended with a broadcast callback
  • src/storage/config.tsWhatsAppConfig interface with phoneNumberId, accessToken, verifyToken, appSecret (for webhook signature verification), autoReply, autoReplySessionId, autoReplyModel added to JclawConfig
  • src/channels/types.ts — added error?: string to DeliveryResult

Frontend

  • /whatsapp page — credential form (Phone Number ID, access token, verify token, app secret), webhook URL ready-to-copy, auto-reply toggle (accessible <input type="checkbox" role="switch" aria-checked>), test send, live message log (inbound cyan / outbound amber, real-time via WS)
  • Overview: live processing bar — "STREAM CHUNKS / SEC" (accurately reflects chat.token event count, not individual tokens), "OUTPUT TODAY" seeded from metrics.aggregation with a midnight cutoff on page load then incremented by live WS events, last model, last provider
  • Overview: Framework Status panel — 8 clickable FrameworkBadge cards (Sandbox, Red Team, MCP, WhatsApp, Pipeline, Evals, Fine-Tune, Embeddings) with live active/inactive state pulled from existing RPC calls
  • Sidebar — new "channels" section with WhatsApp entry

Docs

  • README.md — 5-step WhatsApp setup guide (including App Secret), updated architecture diagram, complete dashboard pages table, protocol methods reference, updated project structure
  • replit.md — rewritten to reflect all current features and WhatsApp setup steps
  • .gitignoreweb/tsconfig.tsbuildinfo excluded (build artifact)

Copilot AI and others added 2 commits April 11, 2026 14:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new first-class WhatsApp channel (config, send, webhook ingest, live log) and extends the dashboard Overview with real-time “processing” indicators and a framework status panel.

Changes:

  • Implement WhatsApp Cloud API outbound adapter, webhook endpoints, and in-memory message store with new whatsapp.* RPC methods.
  • Add /whatsapp dashboard page and wire it into the sidebar + routes.
  • Add Overview live processing bar + Framework Status badges driven by WebSocket events and existing RPC calls; update docs accordingly.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
web/tsconfig.tsbuildinfo Updated TS incremental build info to include new page entry (but is a build artifact).
web/src/pages/WhatsApp.tsx New WhatsApp configuration + test-send + live message log UI via WS events/RPC.
web/src/pages/Overview.tsx Adds live processing bar (tokens/sec etc.) and Framework Status badge panel; subscribes to WS events.
web/src/App.tsx Adds WhatsApp route and new sidebar “channels” section entry.
src/storage/config.ts Introduces WhatsAppConfig + defaults and adds it to JclawConfig.
src/gate/whatsapp-store.ts Adds shared in-process WhatsApp message array for webhook + RPC handlers.
src/gate/server.ts Adds GET/POST /webhook/whatsapp handlers; broadcasts inbound messages to WS clients.
src/gate/protocol.ts Adds whatsapp.config.get/set, whatsapp.send, whatsapp.messages.list RPC methods.
src/channels/types.ts Extends DeliveryResult with optional error field.
src/channels/plugins/whatsapp.ts New WhatsApp outbound adapter calling Meta Graph API /{phoneNumberId}/messages.
replit.md Updates project/docs to include WhatsApp integration and dashboard additions.
README.md Updates feature list, architecture diagram, pages table, and adds WhatsApp setup + protocol reference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/tsconfig.tsbuildinfo
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/ws.ts","./src/pages/Activity.tsx","./src/pages/Chat.tsx","./src/pages/Datasets.tsx","./src/pages/EmbedSearch.tsx","./src/pages/Evals.tsx","./src/pages/FineTune.tsx","./src/pages/Mcp.tsx","./src/pages/Metrics.tsx","./src/pages/Overview.tsx","./src/pages/Prompts.tsx","./src/pages/Providers.tsx","./src/pages/Sandbox.tsx","./src/pages/Search.tsx","./src/pages/SessionDetail.tsx","./src/pages/Sessions.tsx","./src/pages/Templates.tsx","./src/pages/Terminal.tsx"],"version":"5.9.3"} No newline at end of file
{"root":["./src/App.tsx","./src/main.tsx","./src/ws.ts","./src/pages/Activity.tsx","./src/pages/Chat.tsx","./src/pages/Datasets.tsx","./src/pages/EmbedSearch.tsx","./src/pages/Evals.tsx","./src/pages/FineTune.tsx","./src/pages/Mcp.tsx","./src/pages/Metrics.tsx","./src/pages/Overview.tsx","./src/pages/Prompts.tsx","./src/pages/Providers.tsx","./src/pages/Sandbox.tsx","./src/pages/Search.tsx","./src/pages/SessionDetail.tsx","./src/pages/Sessions.tsx","./src/pages/Templates.tsx","./src/pages/Terminal.tsx","./src/pages/WhatsApp.tsx"],"errors":true,"version":"6.0.2"} No newline at end of file
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

tsconfig.tsbuildinfo is a TypeScript incremental build artifact and typically should not be committed (it creates noisy diffs and can be machine-specific). Consider removing it from git and adding it to .gitignore (or configuring the web build to not emit it in tracked paths).

Suggested change
{"root":["./src/App.tsx","./src/main.tsx","./src/ws.ts","./src/pages/Activity.tsx","./src/pages/Chat.tsx","./src/pages/Datasets.tsx","./src/pages/EmbedSearch.tsx","./src/pages/Evals.tsx","./src/pages/FineTune.tsx","./src/pages/Mcp.tsx","./src/pages/Metrics.tsx","./src/pages/Overview.tsx","./src/pages/Prompts.tsx","./src/pages/Providers.tsx","./src/pages/Sandbox.tsx","./src/pages/Search.tsx","./src/pages/SessionDetail.tsx","./src/pages/Sessions.tsx","./src/pages/Templates.tsx","./src/pages/Terminal.tsx","./src/pages/WhatsApp.tsx"],"errors":true,"version":"6.0.2"}

Copilot uses AI. Check for mistakes.
Comment thread web/src/pages/WhatsApp.tsx Outdated
autoComplete="off"
/>
<div style={{ fontSize: "10px", color: "var(--text3)", marginTop: "3px" }}>
Stored encrypted in <code>~/.jclaw/config.json</code>
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This UI copy claims the access token is “Stored encrypted”, but src/storage/config.ts currently writes ~/.jclaw/config.json as plain JSON via writeFileSync with no encryption. Please adjust the text to accurately describe storage (or implement encryption at rest before claiming it).

Suggested change
Stored encrypted in <code>~/.jclaw/config.json</code>
Stored in <code>~/.jclaw/config.json</code>

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +163
// Real-time token stream tracking
const offEvent = onEvent((event, payload) => {
if (event === "chat.token") {
const p = payload as { token: string };
if ((p.token?.length ?? 0) > 0) {
setRecentTokens((prev) => {
const now = Date.now();
return [...prev.filter((t) => now - t.ts < 5_000), { ts: now }];
});
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The live “TOKENS / SEC” value is derived from counting chat.token events over the last 5 seconds, but chat.token payloads are streaming chunks (and can even be the full message in non-streaming fallback), not guaranteed to be a single token. Consider renaming the metric (e.g. “stream chunks/sec”) or emitting an explicit token count from the server if you want true tokens/sec.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +175
if (event === "metrics.sample") {
const rec = payload as { outputTokens?: number; model?: string; provider?: string };
if (rec.outputTokens) {
setLive((prev) => ({
...prev,
totalOutputToday: prev.totalOutputToday + (rec.outputTokens ?? 0),
lastModel: rec.model ?? prev.lastModel,
lastProvider: rec.provider ?? prev.lastProvider,
}));
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

“OUTPUT TODAY” is currently computed by incrementing totalOutputToday from incoming metrics.sample events starting at 0 when the page loads, so it reflects “output tokens since this page load” (and only when outputTokens is truthy), not “today”. Please either relabel it or source a real ‘today’ aggregate from the backend (e.g. query persisted metrics with a midnight cutoff).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +15
/** Shared in-process store for WhatsApp messages (inbound + outbound). */

export interface WhatsAppMessage {
id: string;
from: string;
to?: string;
direction: "inbound" | "outbound";
text: string;
timestamp: number;
status: "received" | "sent" | "failed";
error?: string;
}

/** Mutable array shared between the webhook handler and protocol handlers. */
export const whatsappMessages: WhatsAppMessage[] = [];
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This in-process store is described as a “ring buffer (max 500)”, but the exported whatsappMessages array has no built-in max length enforcement. Either implement push/unshift helpers in this module that enforce the cap, or update the comment so the invariant is accurate.

Copilot uses AI. Check for mistakes.
Comment thread src/gate/server.ts Outdated
Comment on lines +102 to +103
const from = String(m.from ?? "");
const id = String(m.id ?? "");
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

If m.id is missing/empty, this code will store messages with id === "". The frontend dedupes by id, so subsequent empty-id messages will be dropped in the UI. Consider validating required fields (at least id and from) and skipping records that don’t meet the expected webhook schema.

Suggested change
const from = String(m.from ?? "");
const id = String(m.id ?? "");
const from = String(m.from ?? "").trim();
const id = String(m.id ?? "").trim();
if (!from || !id) continue;

Copilot uses AI. Check for mistakes.
Comment thread src/gate/protocol.ts Outdated
Comment on lines +1136 to +1140
// Mask the access token before sending to client
return ok(req.id, {
config: {
...effective,
accessToken: maskKey(effective.accessToken) ?? ""
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

whatsapp.config.get masks accessToken, but it returns verifyToken unmasked as part of ...effective. Since the dashboard WS currently has no auth layer, any connected client can read the webhook verify token. Consider masking verifyToken as well (or omitting it and only returning a boolean like hasVerifyToken).

Suggested change
// Mask the access token before sending to client
return ok(req.id, {
config: {
...effective,
accessToken: maskKey(effective.accessToken) ?? ""
// Mask secret tokens before sending to client
return ok(req.id, {
config: {
...effective,
accessToken: maskKey(effective.accessToken) ?? "",
verifyToken: maskKey(effective.verifyToken) ?? ""

Copilot uses AI. Check for mistakes.
Comment thread src/gate/protocol.ts Outdated
Comment on lines +1181 to +1184
whatsappMessages.unshift(record);
if (whatsappMessages.length > 500) whatsappMessages.length = 500;
ctx.socket.send(JSON.stringify({ type: "event", event: "whatsapp.message", payload: record }));
return ok(req.id, { result, record });
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Outbound sends only emit the whatsapp.message event back to the requesting socket (ctx.socket). If multiple dashboards are connected, other clients won’t see outbound messages in real time (while inbound webhook messages are broadcast to all clients). Consider broadcasting outbound records to all connected WS clients for consistency.

Copilot uses AI. Check for mistakes.
Comment thread src/channels/plugins/whatsapp.ts Outdated
Comment on lines +6 to +9
// Node 18+ ships global fetch. Use `as any` cast to avoid requiring lib:"dom".
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeFetch = (globalThis as any).fetch as (url: string, init?: Record<string, unknown>) => Promise<{ ok: boolean; status: number; statusText: string; json: () => Promise<unknown> }>;

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

nodeFetch is pulled from globalThis.fetch without a runtime guard. If this code runs in an environment where fetch is unavailable/disabled, it will throw at call-time with a cryptic error. Consider checking for globalThis.fetch and returning a clear DeliveryResult error when it’s missing.

Copilot uses AI. Check for mistakes.
Comment thread web/src/pages/WhatsApp.tsx Outdated
Comment on lines +220 to +233
<div
onClick={() => setCfg({ ...cfg, autoReply: !cfg.autoReply })}
style={{
width: "38px", height: "20px", borderRadius: "10px", flexShrink: 0,
background: cfg.autoReply ? "var(--green)" : "var(--border)",
position: "relative", cursor: "pointer", transition: "background 0.2s",
}}
>
<div style={{
width: "14px", height: "14px", borderRadius: "50%", background: "#fff",
position: "absolute", top: "3px",
left: cfg.autoReply ? "21px" : "3px", transition: "left 0.2s",
}} />
</div>
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The auto-reply toggle is implemented as a clickable <div> with no keyboard handling or ARIA semantics, so it’s not accessible to keyboard/screen-reader users. Consider using a real <input type="checkbox"> (visually styled) or a <button role="switch" aria-checked={...}> with proper key handlers.

Suggested change
<div
onClick={() => setCfg({ ...cfg, autoReply: !cfg.autoReply })}
style={{
width: "38px", height: "20px", borderRadius: "10px", flexShrink: 0,
background: cfg.autoReply ? "var(--green)" : "var(--border)",
position: "relative", cursor: "pointer", transition: "background 0.2s",
}}
>
<div style={{
width: "14px", height: "14px", borderRadius: "50%", background: "#fff",
position: "absolute", top: "3px",
left: cfg.autoReply ? "21px" : "3px", transition: "left 0.2s",
}} />
</div>
<input
type="checkbox"
checked={cfg.autoReply}
onChange={(e) => setCfg({ ...cfg, autoReply: e.target.checked })}
style={{
width: "38px",
height: "20px",
borderRadius: "10px",
flexShrink: 0,
appearance: "none",
WebkitAppearance: "none",
margin: 0,
border: "none",
outline: "none",
background: cfg.autoReply ? "var(--green)" : "var(--border)",
cursor: "pointer",
transition: "background 0.2s",
boxShadow: cfg.autoReply
? "inset 21px 0 0 3px #fff"
: "inset 3px 0 0 3px #fff",
}}
/>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

@Jacobcdsmith Jacobcdsmith left a comment

Choose a reason for hiding this comment

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

@copilot let's apply suggested edits

…ty, module hygiene

Agent-Logs-Url: https://github.com/Jacobcdsmith/jclaw-framework/sessions/5506eb0f-f53b-42ce-b7d8-faf1df3d0666

Co-authored-by: Jacobcdsmith <88069592+Jacobcdsmith@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 11, 2026

@copilot let's apply suggested edits

All 11 suggested edits applied in commit 8f3015a:

  • web/tsconfig.tsbuildinfo — added to .gitignore, untracked from git
  • "Stored encrypted" → corrected to "Stored in ~/.jclaw/config.json"
  • "TOKENS / SEC" → renamed to "STREAM CHUNKS / SEC" (accurately reflects chat.token event counting, not individual tokens)
  • "OUTPUT TODAY" → now seeded on page load from metrics.aggregation with a midnight cutoff via the backend; live events continue to increment it
  • Store cap → moved the 500-message enforcement into whatsapp-store.ts as a pushWhatsAppMessage() helper; all callers updated
  • Webhook signature verification → added X-Hub-Signature-256 HMAC-SHA256 check using timingSafeEqual; new appSecret field added to WhatsAppConfig and the dashboard config form
  • Field validation → inbound messages with empty id or from are skipped
  • verifyToken maskingwhatsapp.config.get now masks verifyToken and appSecret alongside accessToken
  • Broadcast outbound → added broadcast to ProtocolContext; whatsapp.send now broadcasts to all WS clients (not just the requesting socket)
  • fetch guard_globalFetch is typed as | undefined and guarded at call-time with a descriptive error return
  • Accessible toggle → replaced <div onClick> with <input type="checkbox" role="switch" aria-checked> styled via CSS appearance: none

@Jacobcdsmith
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 2, 2026

@copilot apply changes based on the comments in this thread

All changes from that review thread were already applied in commit 8f3015a — every one of the 11 items is in place:

  • web/tsconfig.tsbuildinfo added to .gitignore and untracked
  • "Stored encrypted" → "Stored in ~/.jclaw/config.json"
  • "TOKENS / SEC" → "STREAM CHUNKS / SEC"
  • "OUTPUT TODAY" seeded from metrics.aggregation with midnight cutoff
  • pushWhatsAppMessage() helper enforces the 500-item cap in whatsapp-store.ts
  • X-Hub-Signature-256 HMAC-SHA256 webhook signature verification + appSecret field
  • Inbound messages with empty id or from are skipped
  • verifyToken and appSecret masked in whatsapp.config.get
  • Outbound sends broadcast to all WS clients via ctx.broadcast
  • _globalFetch guarded at call-time with descriptive error
  • Auto-reply toggle replaced with <input type="checkbox" role="switch" aria-checked>

Copilot AI requested a review from Jacobcdsmith May 2, 2026 07:57
@Jacobcdsmith Jacobcdsmith marked this pull request as ready for review May 2, 2026 08:11
@Jacobcdsmith Jacobcdsmith merged commit 3fc8f6d into main May 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants