feat: Add OpsAgent, whatsapp-gateway, and hq-dashboard

This commit is contained in:
Antigravity 2026-05-28 21:54:14 -04:00
parent 65b2316dad
commit 5fb2afec1d
25 changed files with 2644 additions and 6 deletions

View File

@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ops-worker
namespace: ai-agents
labels:
app.kubernetes.io/name: ops-worker
agentic-os.io/agent: ops-agent
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ops-worker
template:
metadata:
labels:
app.kubernetes.io/name: ops-worker
agentic-os.io/agent: ops-agent
spec:
serviceAccountName: default
initContainers:
- name: git-clone
image: alpine/git
command:
- "sh"
- "-c"
- "git clone http://192.168.8.248:3000/deepkoluguri/agentic-os.git /workspace && cp -r /workspace/agents/ops_agent/* /app/"
volumeMounts:
- name: app-code
mountPath: /app
containers:
- name: ops-worker
image: python:3.11-slim
imagePullPolicy: Always
command: ["sh", "-c", "pip install -r /app/requirements.txt && cd /app && python -m ops_agent.temporal.worker"]
volumeMounts:
- name: app-code
mountPath: /app
env:
- name: TEMPORAL_ADDRESS
value: "temporal-frontend.ai-core.svc.cluster.local:7233"
- name: TEMPORAL_NAMESPACE
value: "default"
- name: PYTHONUNBUFFERED
value: "1"
- name: OPS_TASK_QUEUE
value: "ops"
- name: MOCK_OPS
value: "false" # Set to false to run actual checks in the cluster
- name: LOCAL_WHATSAPP_URL
value: "http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message"
- name: LITELLM_BASE_URL
value: "http://ollama.ai-core.svc.cluster.local:11434/v1"
- name: GUMBO_LLM_MODEL
value: "qwen2.5:3b"
- name: LITELLM_API_KEY
valueFrom:
secretKeyRef:
name: gumbo-litellm
key: api_key
- name: LANGGRAPH_CHECKPOINT_URI
valueFrom:
secretKeyRef:
name: gumbo-checkpoint-db
key: uri
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
volumes:
- name: app-code
emptyDir: {}

View File

@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: whatsapp-gateway
namespace: ai-agents
labels:
app: whatsapp-gateway
spec:
replicas: 1
selector:
matchLabels:
app: whatsapp-gateway
template:
metadata:
labels:
app: whatsapp-gateway
spec:
serviceAccountName: default
initContainers:
- name: git-clone
image: alpine/git
command:
- "sh"
- "-c"
- "git clone http://192.168.8.248:3000/deepkoluguri/agentic-os.git /workspace && cp -r /workspace/whatsapp-gateway/* /app/"
volumeMounts:
- name: app-code
mountPath: /app
containers:
- name: whatsapp-gateway
image: node:18-alpine
# Install dependencies needed by Puppeteer inside Alpine
# whatsapp-web.js requires Chromium
command:
- "sh"
- "-c"
- "apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont && cd /app && npm install && PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser node server.js"
volumeMounts:
- name: app-code
mountPath: /app
ports:
- containerPort: 5001
resources:
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "1.5"
memory: 1.5Gi
volumes:
- name: app-code
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: whatsapp-gateway
namespace: ai-agents
spec:
ports:
- port: 5001
targetPort: 5001
selector:
app: whatsapp-gateway

View File

@ -0,0 +1,10 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 PYTHONPATH=/app
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY ops_agent /app/ops_agent
CMD ["python", "-m", "ops_agent.run"]

View File

@ -0,0 +1,10 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 PYTHONPATH=/app
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY ops_agent /app/ops_agent
CMD ["python", "-m", "ops_agent.temporal.worker"]

View File

@ -0,0 +1 @@
# OpsAgent package

View File

@ -0,0 +1,226 @@
import os
import logging
from typing import TypedDict
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from ops_agent.tools import (
k8s_get_status, k8s_get_logs, k8s_restart_pod, k8s_scale_deployment,
proxmox_get_status, proxmox_get_disk_space, proxmox_reboot_container, proxmox_clean_disk
)
logger = logging.getLogger(__name__)
class OpsState(TypedDict):
target_type: str # 'kubernetes' or 'proxmox'
target_id: str # e.g., 'default/web-pod' or VMID '101'
status_info: str # status output
logs: str # logs output
diagnosis: str # LLM diagnosis of issues
proposed_action: str # Proposed action: 'restart_pod', 'scale_deployment', 'reboot_container', 'clean_disk', 'none'
proposed_action_details: str # E.g., pod name, deployment name, or container ID
is_approved: bool # Has user approved it?
action_result: str # Outcome of the action execution
def get_llm():
"""Initializes LLM using environment variables, matching the Gumbo setup."""
api_key = os.environ.get("LITELLM_API_KEY") or os.environ.get("OPENAI_API_KEY", "mock-api-key")
base_url = os.environ.get("LITELLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL", "http://localhost:4000")
model = os.environ.get("OPS_LLM_MODEL") or os.environ.get("GUMBO_LLM_MODEL", "ollama-qwen")
logger.info(f"Initializing ChatOpenAI with model={model}, base_url={base_url}")
return ChatOpenAI(
model=model,
api_key=api_key,
base_url=base_url,
temperature=0.1
)
async def _fetch_status_and_logs(state: OpsState) -> dict:
target_type = state.get("target_type", "").lower()
target_id = state.get("target_id", "")
status_info = ""
logs = ""
if target_type == "kubernetes":
# target_id pattern: "namespace/pod-name" or just "pod-name" (assumes default namespace)
parts = target_id.split("/")
if len(parts) == 2:
ns, pod = parts[0], parts[1]
else:
ns, pod = "default", parts[0]
status_info = k8s_get_status(ns, pod)
logs = k8s_get_logs(ns, pod)
elif target_type == "proxmox":
# target_id is LXC VMID (e.g. "101")
status_info = proxmox_get_status(target_id)
logs = proxmox_get_disk_space(target_id)
else:
status_info = f"Unknown target type: {target_type}"
logs = "No logs available."
return {"status_info": status_info, "logs": logs}
async def _diagnose_issue(state: OpsState) -> dict:
llm = get_llm()
system_msg = SystemMessage(
content=(
"You are OpsAgent, a senior DevOps engineer and Proxmox/Kubernetes site reliability assistant. "
"Analyze the provided status info and logs, identify the root cause of any problems, and return "
"a concise Markdown diagnosis."
)
)
user_content = (
f"Target Type: {state['target_type']}\n"
f"Target ID: {state['target_id']}\n\n"
f"=== STATUS INFO ===\n{state['status_info']}\n\n"
f"=== LOGS / RESOURCE USAGE ===\n{state['logs']}\n"
)
try:
resp = await llm.ainvoke([system_msg, HumanMessage(content=user_content)])
diagnosis = resp.content if isinstance(resp.content, str) else str(resp.content)
except Exception as e:
logger.error(f"LLM call failed in diagnosis: {e}")
diagnosis = f"LLM error: Failed to diagnose log. Raw error: {str(e)}"
return {"diagnosis": diagnosis}
async def _propose_remediation(state: OpsState) -> dict:
llm = get_llm()
system_msg = SystemMessage(
content=(
"You are OpsAgent. Based on the diagnosis, propose a specific remediation action from the following list:\n"
"- restart_pod (Kubernetes pods crash/hangs)\n"
"- scale_deployment (K8s capacity issues)\n"
"- reboot_container (Proxmox container stopped/unresponsive)\n"
"- clean_disk (Proxmox/LXC container running out of disk space: Use% >= 90%)\n"
"- none (No actions required, target is running fine)\n\n"
"Respond ONLY with a JSON object: "
'{"action": "restart_pod"|"scale_deployment"|"reboot_container"|"clean_disk"|"none", '
'"details": "brief reason or extra target parameter"}'
)
)
user_content = f"Diagnosis:\n{state['diagnosis']}"
action = "none"
details = "No action proposed."
try:
resp = await llm.ainvoke([system_msg, HumanMessage(content=user_content)])
content = resp.content if isinstance(resp.content, str) else str(resp.content)
# Parse JSON
import json
# Strip potential markdown fences
clean_content = content.replace("```json", "").replace("```", "").strip()
data = json.loads(clean_content)
action = data.get("action", "none")
details = data.get("details", "")
# Dispatch WhatsApp Alert to phone if an anomaly/remediation is flagged
if action != "none":
try:
from ops_agent.tools import send_whatsapp_alert
alert_msg = (
f"⚠️ *OPSAGENT THREAT WARNING* ⚠️\n\n"
f"*Target:* {state['target_type'].upper()} ({state['target_id']})\n"
f"*Diagnosis:* {details}\n"
f"*Proposed Action:* {action.replace('_', ' ').upper()}\n\n"
f"Login to the Agentic OS Dashboard to authorize or reject this mitigation."
)
send_whatsapp_alert(alert_msg)
except Exception as ex:
logger.error(f"Could not dispatch WhatsApp alert: {ex}")
except Exception as e:
logger.error(f"Failed to propose action via LLM: {e}")
return {"proposed_action": action, "proposed_action_details": details}
async def _execute_remediation(state: OpsState) -> dict:
if not state.get("is_approved", False):
return {"action_result": "Remediation action skipped: Approval was not provided."}
action = state.get("proposed_action", "none")
target_type = state.get("target_type", "")
target_id = state.get("target_id", "")
result = "No action executed."
try:
if target_type == "kubernetes":
parts = target_id.split("/")
ns = parts[0] if len(parts) == 2 else "default"
pod = parts[1] if len(parts) == 2 else parts[0]
if action == "restart_pod":
result = k8s_restart_pod(ns, pod)
elif action == "scale_deployment":
# Default scale to 2 for demo purposes
result = k8s_scale_deployment(ns, pod, 2)
else:
result = f"Action '{action}' is not supported for Kubernetes target."
elif target_type == "proxmox":
if action == "reboot_container":
result = proxmox_reboot_container(target_id)
elif action == "clean_disk":
result = proxmox_clean_disk(target_id)
else:
result = f"Action '{action}' is not supported for Proxmox container VMID {target_id}."
else:
result = f"Unknown target type: {target_type}."
except Exception as e:
result = f"Error during execution: {str(e)}"
return {"action_result": result}
def build_graph_builder() -> StateGraph:
builder = StateGraph(OpsState)
builder.add_node("fetch_status", _fetch_status_and_logs)
builder.add_node("diagnose", _diagnose_issue)
builder.add_node("propose_action", _propose_remediation)
builder.add_node("execute_action", _execute_remediation)
builder.set_entry_point("fetch_status")
builder.add_edge("fetch_status", "diagnose")
builder.add_edge("diagnose", "propose_action")
builder.add_edge("propose_action", "execute_action")
builder.add_edge("execute_action", END)
return builder
async def run_ops_graph(target_type: str, target_id: str, is_approved: bool = False, conn_string: str = None) -> OpsState:
builder = build_graph_builder()
initial_state = {
"target_type": target_type,
"target_id": target_id,
"status_info": "",
"logs": "",
"diagnosis": "",
"proposed_action": "none",
"proposed_action_details": "",
"is_approved": is_approved,
"action_result": ""
}
if conn_string:
async with AsyncPostgresSaver.from_conn_string(conn_string) as checkpointer:
await checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
return await graph.ainvoke(
initial_state,
config={"configurable": {"thread_id": f"ops-{target_type}-{target_id}"}},
)
else:
graph = builder.compile()
return await graph.ainvoke(initial_state)

View File

@ -0,0 +1,63 @@
import asyncio
import argparse
import os
import sys
import json
async def main():
parser = argparse.ArgumentParser(description="Run OpsAgent diagnostics CLI tool.")
parser.add_argument("--target-type", required=True, choices=["kubernetes", "proxmox"], help="Target platform to check.")
parser.add_argument("--target-id", required=True, help="Specific target identifier (e.g., namespace/pod-name or Proxmox container VMID).")
parser.add_argument("--approve", action="store_true", help="Approve and execute proposed remediation.")
parser.add_argument("--real", action="store_true", help="Run real CLI commands instead of mocks.")
args = parser.parse_args()
# Configure logging
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# Set mock mode
if args.real:
os.environ["MOCK_OPS"] = "false"
print("Executing in REAL mode...")
else:
os.environ["MOCK_OPS"] = "true"
print("Executing in MOCK mode...")
# We need to set mock key environment variables if they aren't present
if "LITELLM_API_KEY" not in os.environ:
os.environ["LITELLM_API_KEY"] = "mock-key"
if "LITELLM_BASE_URL" not in os.environ:
os.environ["LITELLM_BASE_URL"] = "http://localhost:4000" # fallback
from ops_agent.graph import run_ops_graph
print(f"Starting OpsAgent check for target type: {args.target_type}, ID: {args.target_id}...")
try:
result = await run_ops_graph(
target_type=args.target_type,
target_id=args.target_id,
is_approved=args.approve
)
print("\n==================================================")
print(" RESULTS ")
print("==================================================")
print(f"Target Check: {result['target_type'].upper()} ({result['target_id']})")
print("\n--- DIAGNOSIS ---")
print(result["diagnosis"])
print("\n--- PROPOSED ACTION ---")
print(f"Action: {result['proposed_action']}")
print(f"Details: {result['proposed_action_details']}")
print("\n--- EXECUTION RESULT ---")
print(result["action_result"])
print("==================================================")
except Exception as e:
print(f"\nExecution failed: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1 @@
# Temporal package initialization for OpsAgent

View File

@ -0,0 +1,58 @@
from __future__ import annotations
import os
from typing import Any
from temporalio import activity
from ops_agent.graph import run_ops_graph
@activity.defn
async def run_ops_diagnose_activity(params: dict[str, Any]) -> dict[str, Any]:
"""Runs status check, log fetch, and LLM diagnosis."""
target_type = params["target_type"]
target_id = params["target_id"]
# We do NOT run remediation yet, just diagnosis and proposed action
state = await run_ops_graph(
target_type=target_type,
target_id=target_id,
is_approved=False
)
return {
"target_type": state["target_type"],
"target_id": state["target_id"],
"status_info": state["status_info"],
"logs": state["logs"],
"diagnosis": state["diagnosis"],
"proposed_action": state["proposed_action"],
"proposed_action_details": state["proposed_action_details"]
}
@activity.defn
async def run_ops_remediate_activity(params: dict[str, Any]) -> dict[str, Any]:
"""Runs remediation action once approved."""
target_type = params["target_type"]
target_id = params["target_id"]
proposed_action = params["proposed_action"]
from ops_agent.graph import build_graph_builder
builder = build_graph_builder()
graph = builder.compile()
# Run the graph directly starting with the execute node by feeding the state
state = await graph.ainvoke({
"target_type": target_type,
"target_id": target_id,
"status_info": params.get("status_info", ""),
"logs": params.get("logs", ""),
"diagnosis": params.get("diagnosis", ""),
"proposed_action": proposed_action,
"proposed_action_details": params.get("proposed_action_details", ""),
"is_approved": True,
"action_result": ""
})
return {
"action_result": state["action_result"]
}

View File

@ -0,0 +1,48 @@
from __future__ import annotations
import asyncio
import os
import logging
from temporalio.client import Client
from temporalio.worker import Worker
from ops_agent.temporal.workflows import OpsRemediateWorkflow
from ops_agent.temporal.activities import (
run_ops_diagnose_activity,
run_ops_remediate_activity
)
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
async def _main() -> None:
address = os.environ.get("TEMPORAL_ADDRESS", "temporal-frontend.ai-core.svc.cluster.local:7233")
namespace = os.environ.get("TEMPORAL_NAMESPACE", "default")
task_queue = os.environ.get("OPS_TASK_QUEUE", "ops")
logger.info(f"Connecting to Temporal cluster at {address} (Namespace: {namespace})...")
try:
client = await Client.connect(address, namespace=namespace)
logger.info("Successfully connected to Temporal!")
except Exception as e:
logger.error(f"Failed to connect to Temporal: {e}")
return
worker = Worker(
client,
task_queue=task_queue,
workflows=[OpsRemediateWorkflow],
activities=[
run_ops_diagnose_activity,
run_ops_remediate_activity,
],
)
logger.info(f"Starting OpsAgent worker on queue '{task_queue}'...")
await worker.run()
def main() -> None:
asyncio.run(_main())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,100 @@
from __future__ import annotations
from datetime import timedelta
from temporalio import workflow
with workflow.unsafe.imports_passed_through():
from ops_agent.temporal.activities import (
run_ops_diagnose_activity,
run_ops_remediate_activity
)
@workflow.defn
class OpsRemediateWorkflow:
"""Orchestrates OpsAgent diagnosis, waits for human operator approval signal, and executes remediation."""
def __init__(self) -> None:
self._approved = False
self._rejected = False
@workflow.signal
def approve(self) -> None:
self._approved = True
@workflow.signal
def reject(self) -> None:
self._rejected = True
@workflow.run
async def run(self, target_type: str, target_id: str) -> dict:
# Step 1: Diagnose issues
params = {"target_type": target_type, "target_id": target_id}
diagnose_res = await workflow.execute_activity(
run_ops_diagnose_activity,
params,
start_to_close_timeout=timedelta(minutes=5),
)
proposed_action = diagnose_res.get("proposed_action", "none")
# Step 2: Check if remediation is proposed. If 'none', exit.
if proposed_action == "none":
return {
"status": "completed",
"diagnosis": diagnose_res["diagnosis"],
"action_proposed": "none",
"action_executed": "none",
"result": "No problems detected. No action needed."
}
# Step 3: Approval Gate (wait for approval signal or reject signal or timeout)
# Timeout after 24 hours (or shorter in test/production depending on requirements)
approval_timeout = timedelta(hours=2)
workflow.logger.info(
f"Action proposed: {proposed_action}. Waiting for approval signal on workflow ID: {workflow.info().workflow_id}"
)
# Wait for approval condition
try:
is_condition_met = await workflow.wait_condition(
lambda: self._approved or self._rejected,
timeout=approval_timeout
)
except Exception:
# wait_condition times out
is_condition_met = False
if not is_condition_met:
return {
"status": "timed_out",
"diagnosis": diagnose_res["diagnosis"],
"action_proposed": proposed_action,
"action_executed": "none",
"result": "Timed out waiting for administrator approval."
}
if self._rejected:
return {
"status": "rejected",
"diagnosis": diagnose_res["diagnosis"],
"action_proposed": proposed_action,
"action_executed": "none",
"result": "Remediation action rejected by administrator."
}
# Step 4: Execute remediation
remed_params = {**diagnose_res, "is_approved": True}
exec_res = await workflow.execute_activity(
run_ops_remediate_activity,
remed_params,
start_to_close_timeout=timedelta(minutes=5),
)
return {
"status": "remediated",
"diagnosis": diagnose_res["diagnosis"],
"action_proposed": proposed_action,
"action_executed": proposed_action,
"result": exec_res["action_result"]
}

View File

@ -0,0 +1,202 @@
import os
import subprocess
import logging
logger = logging.getLogger(__name__)
def run_local_command(args):
"""Executes a command on the local system."""
try:
res = subprocess.run(args, capture_output=True, text=True, timeout=15)
if res.returncode == 0:
return res.stdout
else:
return f"Error executing command: {res.stderr}"
except Exception as e:
return f"Failed to run command locally: {str(e)}"
def run_ssh_command(host, username, command, password=None, key_filename=None):
"""Runs a command via SSH on a remote host (e.g., Proxmox node)."""
try:
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Connect using password or key
ssh.connect(
hostname=host,
username=username,
password=password,
key_filename=key_filename,
timeout=10
)
stdin, stdout, stderr = ssh.exec_command(command)
out = stdout.read().decode('utf-8')
err = stderr.read().decode('utf-8')
ssh.close()
if err and not out:
return f"SSH Error: {err}"
return out
except Exception as e:
return f"SSH connection failed: {str(e)}"
# --- Kubernetes Tools ---
def k8s_get_status(namespace: str, pod_name: str = None) -> str:
"""Gets Kubernetes pod status in the specified namespace."""
if os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock K8s] Fetching status...")
if pod_name:
return f"NAME: {pod_name}\nREADY: 0/1\nSTATUS: CrashLoopBackOff\nRESTARTS: 5\nAGE: 2h"
return f"NAME\tREADY\tSTATUS\tRESTARTS\tAGE\nweb-pod-xyz\t0/1\tCrashLoopBackOff\t5\t2h\ndb-pod-abc\t1/1\tRunning\t0\t5d"
# Real execution
cmd = ["kubectl", "get", "pods", "-n", namespace]
if pod_name:
cmd.append(pod_name)
return run_local_command(cmd)
def k8s_get_logs(namespace: str, pod_name: str) -> str:
"""Gets Kubernetes pod logs."""
if os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock K8s] Fetching logs...")
return (
"2026-05-29 01:00:00 [INFO] Starting up server...\n"
"2026-05-29 01:00:01 [INFO] Connecting to database...\n"
"2026-05-29 01:00:03 [ERROR] Database connection failed: Connection refused by 10.96.20.10:5432\n"
"2026-05-29 01:00:03 [FATAL] Exiting server due to database unavailability."
)
# Real execution
cmd = ["kubectl", "logs", pod_name, "-n", namespace, "--tail=100"]
return run_local_command(cmd)
def k8s_restart_pod(namespace: str, pod_name: str) -> str:
"""Restarts a Kubernetes pod by deleting it (K8s will recreate it)."""
if os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock K8s] Restarting pod...")
return f"Mock: Pod {namespace}/{pod_name} successfully deleted (restarted)."
cmd = ["kubectl", "delete", "pod", pod_name, "-n", namespace]
return run_local_command(cmd)
def k8s_scale_deployment(namespace: str, deployment: str, replicas: int) -> str:
"""Scales a deployment to the target number of replicas."""
if os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock K8s] Scaling deployment...")
return f"Mock: Deployment {namespace}/{deployment} successfully scaled to {replicas} replicas."
cmd = ["kubectl", "scale", f"deployment/{deployment}", f"--replicas={replicas}", "-n", namespace]
return run_local_command(cmd)
# --- Proxmox Tools ---
def get_proxmox_ssh_creds():
"""Retrieves SSH configs for Proxmox node from environment."""
return {
"host": os.environ.get("PROXMOX_SSH_HOST"),
"user": os.environ.get("PROXMOX_SSH_USER", "root"),
"password": os.environ.get("PROXMOX_SSH_PASSWORD"),
"key": os.environ.get("PROXMOX_SSH_KEY")
}
def proxmox_get_status(vmid: str) -> str:
"""Checks LXC container status in Proxmox."""
creds = get_proxmox_ssh_creds()
if not creds["host"] or os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock Proxmox] Fetching status for VMID %s...", vmid)
return f"status: stopped\nname: app-container-{vmid}\nmem: 2048MB\ndisk: 20GB\nrunning: false"
cmd = f"pct status {vmid}"
return run_ssh_command(creds["host"], creds["user"], cmd, password=creds["password"], key_filename=creds["key"])
def proxmox_get_disk_space(vmid: str) -> str:
"""Queries disk space for the LXC container."""
creds = get_proxmox_ssh_creds()
if not creds["host"] or os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock Proxmox] Fetching disk space for VMID %s...", vmid)
return (
"Filesystem Size Used Avail Use% Mounted on\n"
"/dev/loop0 20G 19G 0.2G 99% /\n"
"none 4.0G 0 4.0G 0% /dev"
)
cmd = f"pct exec {vmid} -- df -h /"
return run_ssh_command(creds["host"], creds["user"], cmd, password=creds["password"], key_filename=creds["key"])
def proxmox_reboot_container(vmid: str) -> str:
"""Reboots a Proxmox LXC container."""
creds = get_proxmox_ssh_creds()
if not creds["host"] or os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock Proxmox] Rebooting LXC container %s...", vmid)
return f"Mock: Container {vmid} successfully rebooted."
cmd = f"pct reboot {vmid}"
return run_ssh_command(creds["host"], creds["user"], cmd, password=creds["password"], key_filename=creds["key"])
def proxmox_clean_disk(vmid: str) -> str:
"""Performs common disk cleanup operations inside a Proxmox container."""
creds = get_proxmox_ssh_creds()
if not creds["host"] or os.environ.get("MOCK_OPS", "true").lower() == "true":
logger.info("[Mock Proxmox] Cleaning up disk inside container %s...", vmid)
return "Mock: Ran 'apt-get clean' and removed temporary files. Disk usage reduced to 75%."
# Perform cleanup inside LXC
cleanup_cmd = "pct exec {} -- bash -c 'apt-get clean && rm -rf /tmp/* && rm -rf /var/tmp/*'"
cmd = cleanup_cmd.format(vmid)
return run_ssh_command(creds["host"], creds["user"], cmd, password=creds["password"], key_filename=creds["key"])
def send_whatsapp_alert(message_body: str) -> str:
"""Dispatches a WhatsApp alert using the local gateway (if running) or falls back to Twilio."""
admin_number = os.environ.get("ADMIN_WHATSAPP_NUMBER") or os.environ.get("PHYSICIAN_WHATSAPP_NUMBER", "whatsapp:+14085505485")
# 1. Attempt local WhatsApp Web Gateway first (if enabled/running)
use_local = os.environ.get("USE_LOCAL_WHATSAPP", "true").lower() == "true"
gateway_url = os.environ.get("LOCAL_WHATSAPP_URL", "http://localhost:5001/send-message")
if use_local:
try:
import urllib.request
import json
payload = json.dumps({"to": admin_number, "message": message_body}).encode('utf-8')
req = urllib.request.Request(
gateway_url,
data=payload,
headers={'Content-Type': 'application/json'},
method='POST'
)
# Short timeout to avoid hanging if gateway is offline
with urllib.request.urlopen(req, timeout=8) as response:
res_data = json.loads(response.read().decode('utf-8'))
if res_data.get("success"):
logger.info(f"[OpsAgent] Local WhatsApp alert dispatched successfully to: {res_data.get('recipient')}")
return f"Alert sent via local gateway (JID: {res_data.get('recipient')})"
except Exception as e:
logger.warning(f"[OpsAgent] Local WhatsApp Web Gateway failed or is offline: {str(e)}. Falling back to Twilio...")
# 2. Twilio Fallback
account_sid = os.environ.get("TWILIO_ACCOUNT_SID", "AC601e1962dc8417ddb3ae53cd8d865020")
auth_token = os.environ.get("TWILIO_AUTH_TOKEN", "08a68571a251d6d62f60dc83fc725b5a")
twilio_from = os.environ.get("TWILIO_WHATSAPP_FROM") or os.environ.get("TWILIO_FROM_NUMBER", "whatsapp:+14155238886")
if not (account_sid and auth_token and twilio_from and admin_number):
logger.warning("[OpsAgent] Twilio credentials or admin number not set. Skipping WhatsApp alert.")
return "Twilio credentials not configured."
try:
from twilio.rest import Client
client = Client(account_sid, auth_token)
message = client.messages.create(
from_=twilio_from,
body=message_body,
to=admin_number
)
logger.info(f"[OpsAgent] WhatsApp alert sent successfully! SID: {message.sid}")
return f"Alert sent (SID: {message.sid})"
except Exception as e:
logger.error(f"[OpsAgent] Failed to send WhatsApp alert: {str(e)}")
return f"Failed to send alert: {str(e)}"

View File

@ -0,0 +1,24 @@
[project]
name = "ops-agent"
version = "0.1.0"
description = "OpsAgent — Infrastructure & DevOps monitoring agent for Proxmox and Kubernetes"
requires-python = ">=3.11"
dependencies = [
"langgraph>=0.2.0",
"langchain-core>=0.3.0",
"langchain-openai>=0.2.0",
"temporalio>=1.8.0",
"paramiko>=3.4.0",
"psycopg[binary]>=3.2.0",
"pydantic>=2.0.0",
"twilio>=8.0.0",
]
[project.scripts]
ops-worker = "ops_agent.temporal.worker:main"
ops-run = "ops_agent.run:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

View File

@ -0,0 +1,8 @@
langgraph>=0.2.0
langchain-core>=0.3.0
langchain-openai>=0.2.0
temporalio>=1.8.0
paramiko>=3.4.0
psycopg[binary]>=3.2.0
pydantic>=2.0.0
twilio>=8.0.0

97
agents/trigger_ops.py Normal file
View File

@ -0,0 +1,97 @@
"""Trigger and control OpsAgent workflows from local machine."""
from __future__ import annotations
import asyncio
import os
import sys
import argparse
from temporalio.client import Client
async def connect_client():
address = os.environ.get("TEMPORAL_ADDRESS", "localhost:7233")
namespace = os.environ.get("TEMPORAL_NAMESPACE", "default")
print(f"Connecting to Temporal at {address}...")
return await Client.connect(address, namespace=namespace)
async def start_workflow(args):
client = await connect_client()
wf_id = f"ops-{args.target_type}-{args.target_id.replace('/', '-')}"
print(f"Starting OpsRemediateWorkflow for {args.target_type} target '{args.target_id}'...")
try:
handle = await client.start_workflow(
"OpsRemediateWorkflow",
args.target_type,
args.target_id,
id=wf_id,
task_queue="ops",
)
print(f"Workflow started successfully! Workflow ID: {handle.id}")
print("You can now monitor it, or run 'approve' / 'reject' command to signal it.")
except Exception as e:
print(f"Failed to start workflow: {e}")
async def send_signal(args, signal_type: str):
client = await connect_client()
wf_id = args.workflow_id
print(f"Sending '{signal_type}' signal to workflow {wf_id}...")
try:
handle = client.get_workflow_handle(wf_id)
if signal_type == "approve":
await handle.signal("approve")
else:
await handle.signal("reject")
print(f"Signal '{signal_type}' sent successfully!")
except Exception as e:
print(f"Failed to send signal: {e}")
async def get_result(args):
client = await connect_client()
wf_id = args.workflow_id
print(f"Waiting for results of workflow {wf_id}...")
try:
handle = client.get_workflow_handle(wf_id)
result = await handle.result()
print("\n=== Workflow Final Result ===")
import json
print(json.dumps(result, indent=2))
except Exception as e:
print(f"Failed to retrieve workflow result: {e}")
def main():
parser = argparse.ArgumentParser(description="Trigger and control OpsAgent workflows.")
subparsers = parser.add_subparsers(dest="command", required=True)
# Start command
parser_start = subparsers.add_parser("start", help="Start a diagnostics & remediation workflow.")
parser_start.add_argument("--target-type", required=True, choices=["kubernetes", "proxmox"])
parser_start.add_argument("--target-id", required=True, help="e.g. default/web-pod or VMID like 101")
# Approve command
parser_app = subparsers.add_parser("approve", help="Send approve signal to running workflow.")
parser_app.add_argument("--workflow-id", required=True)
# Reject command
parser_rej = subparsers.add_parser("reject", help="Send reject signal to running workflow.")
parser_rej.add_argument("--workflow-id", required=True)
# Result command
parser_res = subparsers.add_parser("result", help="Wait for and display workflow final result.")
parser_res.add_argument("--workflow-id", required=True)
args = parser.parse_args()
if args.command == "start":
asyncio.run(start_workflow(args))
elif args.command == "approve":
asyncio.run(send_signal(args, "approve"))
elif args.command == "reject":
asyncio.run(send_signal(args, "reject"))
elif args.command == "result":
asyncio.run(get_result(args))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -0,0 +1,187 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--bg-primary: #080a10;
--bg-secondary: #0f131f;
--bg-card: rgba(20, 26, 43, 0.6);
--border-color: rgba(255, 255, 255, 0.08);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-violet: #8b5cf6;
--accent-violet-glow: rgba(139, 92, 246, 0.25);
--accent-cyan: #06b6d4;
--accent-cyan-glow: rgba(6, 182, 212, 0.25);
--accent-emerald: #10b981;
--accent-emerald-glow: rgba(16, 185, 129, 0.2);
--accent-rose: #f43f5e;
--accent-rose-glow: rgba(244, 63, 94, 0.2);
--accent-amber: #f59e0b;
--accent-amber-glow: rgba(245, 158, 11, 0.2);
--font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 3px;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
overflow-x: hidden;
height: 100vh;
}
/* Glassmorphism utility */
.glass {
background: var(--bg-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-color);
border-radius: 12px;
}
.glass-interactive {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-interactive:hover {
border-color: rgba(139, 92, 246, 0.3);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4), 0 0 15px rgba(139, 92, 246, 0.05);
transform: translateY(-2px);
}
/* Accent Glows */
.glow-violet {
box-shadow: 0 0 20px var(--accent-violet-glow);
}
.glow-cyan {
box-shadow: 0 0 20px var(--accent-cyan-glow);
}
/* Status Indicator pulsing */
@keyframes pulse-emerald {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
.pulse-emerald {
animation: pulse-emerald 2s infinite;
background-color: var(--accent-emerald);
}
@keyframes pulse-amber {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
.pulse-amber {
animation: pulse-amber 2s infinite;
background-color: var(--accent-amber);
}
/* Layout Utilities */
.app-container {
display: grid;
grid-template-columns: 260px 1fr;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.sidebar {
background-color: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 24px 16px;
justify-content: space-between;
}
.main-content {
display: flex;
flex-direction: column;
height: 100vh;
overflow-y: auto;
position: relative;
background-image: radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.05), transparent 40%),
radial-gradient(circle at 10% 80%, rgba(6, 182, 212, 0.05), transparent 40%);
}
.header {
height: 70px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px;
background: rgba(8, 10, 16, 0.5);
backdrop-filter: blur(8px);
position: sticky;
top: 0;
z-index: 10;
}
.content-body {
padding: 32px;
flex: 1;
display: flex;
flex-direction: column;
}
/* Typography and utilities */
h1, h2, h3, h4, h5 {
font-weight: 600;
letter-spacing: -0.02em;
}
.mono {
font-family: var(--font-mono);
}
/* Animations */
.fade-in {
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -1,7 +1,14 @@
import './globals.css';
export const metadata = {
title: 'Agentic OS HQ — AI Agent Command Center',
description: 'Control center and dashboard for monitoring and managing autonomous workflows (Bernard, Gumbo, OpsAgent, Curio).',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body style={{ fontFamily: "system-ui", margin: 0, padding: 24 }}>{children}</body>
<body>{children}</body>
</html>
);
}

View File

@ -1,8 +1,811 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import {
Activity,
Terminal,
User,
Cpu,
ShieldAlert,
CheckCircle,
XCircle,
Database,
FileText,
Sparkles,
Send,
RefreshCw,
Play,
GitPullRequest,
Layers,
Settings,
AlertCircle,
Trash2,
HeartPulse,
LogOut,
Maximize2
} from "lucide-react";
export default function Page() {
return (
<main>
<h1>Agentic OS HQ</h1>
<p>Next.js shell scaffold. Wire this UI to Temporal and Langfuse in later iterations.</p>
</main>
const [activeTab, setActiveTab] = useState("dashboard");
// Claw Chat State
const [messages, setMessages] = useState([
{
role: "assistant",
content: "Hello! I am Claw, your local agent. I have access to your Kubernetes namespace and Proxmox cluster nodes. How can I help you today?",
timestamp: "Just now"
}
]);
const [inputValue, setInputValue] = useState("");
const [isClawTyping, setIsClawTyping] = useState(false);
const [clawTerminalLogs, setClawTerminalLogs] = useState([]);
// OpsAgent State
const [pendingApprovals, setPendingApprovals] = useState([
{
id: "ops-req-102",
targetType: "proxmox",
targetId: "102",
action: "clean_disk",
details: "LXC container root filesystem is at 94% disk usage. Recommending apt cache clean and temporary file removal.",
timestamp: "5 mins ago",
status: "pending"
},
{
id: "ops-req-201",
targetType: "kubernetes",
targetId: "curio-app-xyz",
action: "restart_pod",
details: "Pod curio-app-xyz is in CrashLoopBackOff state. Logs indicate: 'Database connection failed: Connection refused by 10.96.20.10:5432'. Recommending pod deletion to trigger k8s reschedule.",
timestamp: "10 mins ago",
status: "pending"
}
]);
const [opsLogs, setOpsLogs] = useState([
"2026-05-29 01:10:00 [OpsAgent] Initialized diagnostic run on K8s cluster...",
"2026-05-29 01:10:15 [OpsAgent] Found Pod 'curio-app-xyz' in CrashLoopBackOff. Emitting warning.",
"2026-05-29 01:12:02 [OpsAgent] Proxmox VMID 102 root usage checked: 94%. Action required."
]);
// Curio HMS State
const [dictationText, setDictationText] = useState(
"Patient is a 45-year-old male presenting with severe, crushing chest pain radiating to his left arm. He has a past medical history of hypertension. Admitting to Cardiac ICU."
);
const [clinicalNote, setClinicalNote] = useState(null);
const [billingCodes, setBillingCodes] = useState([]);
const [isProcessingClinical, setIsProcessingClinical] = useState(false);
// Workflow logs state
const [workflows, setWorkflows] = useState([
{ id: "wf-bernard-001", name: "PRReviewWorkflow", agent: "Bernard", target: "pr-401", status: "completed", result: "Approved with suggestions" },
{ id: "wf-gumbo-002", name: "GumboSummarizeWorkflow", agent: "Gumbo", target: "api-spec.md", status: "completed", result: "Summary persisted in PG" },
{ id: "wf-ops-003", name: "OpsRemediateWorkflow", agent: "OpsAgent", target: "VMID 102", status: "running", result: "Awaiting approval signal" }
]);
const messagesEndRef = useRef(null);
const terminalEndRef = useRef(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isClawTyping]);
useEffect(() => {
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [clawTerminalLogs]);
// Simulated Claw response logic
const handleSendMessage = async (e) => {
e.preventDefault();
if (!inputValue.trim()) return;
const userText = inputValue;
setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]);
setInputValue("");
setIsClawTyping(true);
setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]);
// Parse commands for visual interactions
let responseText = "";
let logs = [];
await new Promise(resolve => setTimeout(resolve, 1500));
const cleanInput = userText.toLowerCase();
if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) {
logs = [
"Connecting to Kubernetes API server...",
"Executing: kubectl get pods -n default",
"Found 2 pods in namespace 'default':",
" - web-pod-xyz 0/1 CrashLoopBackOff 5 2h",
" - db-pod-abc 1/1 Running 0 5d",
"Fetching logs for 'web-pod-xyz'...",
" Error detected: Database connection refused by 10.96.20.10:5432"
];
responseText = "I've checked the Kubernetes pods. Pod `web-pod-xyz` is currently failing due to a database connection refusal. You can use the **OpsAgent** tab to approve a pod restart or scale the DB deployment.";
} else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) {
logs = [
"Establishing SSH connection to Proxmox cluster host 192.168.8.225...",
"Running: pct status 102",
" status: running",
"Running: pct exec 102 -- df -h /",
" Filesystem Size Used Avail Use% Mounted on",
" /dev/loop0 20G 19G 0.2G 94% /",
"Disk usage critically high! Proposed action: clean_disk"
];
responseText = "Proxmox Container ID `102` is running out of disk space (94% used). I have created a pending action under the **OpsAgent Center** to perform automated cleanup. Would you like me to execute it?";
} else if (cleanInput.includes("hello") || cleanInput.includes("hi")) {
responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods` or `diagnose proxmox container 102`.";
} else {
responseText = `I've analyzed your query: "${userText}". I recommend initiating an **OpsRemediateWorkflow** to check the status or scanning your configuration files. Let me know if you would like me to trigger that for you.`;
}
setClawTerminalLogs(prev => [...prev, ...logs, "[Claw] Response formulated successfully."]);
setMessages(prev => [...prev, { role: "assistant", content: responseText, timestamp: "Just now" }]);
setIsClawTyping(false);
};
// Approval Handlers
const handleApprove = async (id, action, target) => {
setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item));
setOpsLogs(prev => [...prev, `2026-05-29 01:14:10 [OpsAgent] RECEIVED APPROVAL SIGNAL for request ${id}`]);
// Simulate execution log
let execLogs = [
`[OpsAgent] Initiating execution of '${action}' on target '${target}'...`,
`[OpsAgent] Sending signal to Temporal workflow...`,
];
if (action === "clean_disk") {
execLogs.push(
"[OpsAgent] SSH into Proxmox node VMID 102...",
"[OpsAgent] running: apt-get clean && rm -rf /tmp/*",
"[OpsAgent] Disk space cleared. Root filesystem usage reduced from 94% to 71%."
);
} else {
execLogs.push(
`[OpsAgent] Running: kubectl delete pod ${target} -n default`,
`[OpsAgent] Pod deleted. K8s controller successfully scheduled a replica.`
);
}
// Delay log write to simulate real execution
for (const log of execLogs) {
await new Promise(resolve => setTimeout(resolve, 800));
setOpsLogs(prev => [...prev, log]);
}
setPendingApprovals(prev => prev.filter(item => item.id !== id));
setWorkflows(prev => prev.map(wf => wf.target === (action === "clean_disk" ? "VMID 102" : target) ? { ...wf, status: "completed", result: "Remediated successfully" } : wf));
};
const handleReject = (id) => {
setPendingApprovals(prev => prev.filter(item => item.id !== id));
setOpsLogs(prev => [...prev, `2026-05-29 01:14:30 [OpsAgent] Request ${id} REJECTED by operator. Action aborted.`]);
};
// Clinical note simulation
const handleProcessClinical = async () => {
setIsProcessingClinical(true);
setClinicalNote(null);
setBillingCodes([]);
await new Promise(resolve => setTimeout(resolve, 1500));
setClinicalNote({
subjective: "Patient is a 45-year-old male presenting with severe, crushing chest pain radiating to his left arm. He reports shortness of breath.",
objective: "Alert and oriented. Reports pain scale 8/10.",
assessment: "Acute Myocardial Infarction suspected.",
plan: "Order EKG, troponin levels. Administer Aspirin 324mg immediately. Admit to Cardiac ICU."
});
setBillingCodes([
{ code: "I21.9", desc: "Acute myocardial infarction, unspecified" },
{ code: "93000", desc: "Electrocardiogram, tracing and report" },
{ code: "99223", desc: "Initial hospital care, high level severity" }
]);
setIsProcessingClinical(false);
};
return (
<div className="app-container">
{/* Sidebar Navigation */}
<aside className="sidebar">
<div style={{ display: "flex", flexDirection: "column", gap: "32px" }}>
{/* Dashboard Title */}
<div style={{ display: "flex", alignItems: "center", gap: "10px", padding: "0 8px" }}>
<div style={{ width: "32px", height: "32px", borderRadius: "8px", background: "linear-gradient(135deg, var(--accent-violet), var(--accent-cyan))", display: "flex", alignItems: "center", justifyContent: "center" }}>
<Terminal size={18} style={{ color: "#fff" }} />
</div>
<div>
<h2 style={{ fontSize: "16px", color: "var(--text-primary)" }}>Agentic OS</h2>
<span style={{ fontSize: "11px", color: "var(--text-muted)", letterSpacing: "1px", textTransform: "uppercase" }}>Control HQ</span>
</div>
</div>
{/* Nav Items */}
<nav style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<button
onClick={() => setActiveTab("dashboard")}
style={{
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
background: activeTab === "dashboard" ? "rgba(139, 92, 246, 0.15)" : "transparent",
color: activeTab === "dashboard" ? "var(--accent-violet)" : "var(--text-secondary)",
fontWeight: activeTab === "dashboard" ? "600" : "500",
transition: "all 0.2s"
}}
>
<Cpu size={18} />
Dashboard Home
</button>
<button
onClick={() => setActiveTab("claw")}
style={{
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
background: activeTab === "claw" ? "rgba(6, 182, 212, 0.15)" : "transparent",
color: activeTab === "claw" ? "var(--accent-cyan)" : "var(--text-secondary)",
fontWeight: activeTab === "claw" ? "600" : "500",
transition: "all 0.2s"
}}
>
<Sparkles size={18} />
Claw Assistant
</button>
<button
onClick={() => setActiveTab("ops")}
style={{
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
background: activeTab === "ops" ? "rgba(245, 158, 11, 0.15)" : "transparent",
color: activeTab === "ops" ? "var(--accent-amber)" : "var(--text-secondary)",
fontWeight: activeTab === "ops" ? "600" : "500",
transition: "all 0.2s"
}}
>
<ShieldAlert size={18} />
OpsAgent Center
{pendingApprovals.length > 0 && (
<span style={{ fontSize: "10px", padding: "2px 6px", borderRadius: "10px", background: "var(--accent-amber)", color: "#000", fontWeight: "700", marginLeft: "auto" }}>
{pendingApprovals.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("curio")}
style={{
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
background: activeTab === "curio" ? "rgba(16, 185, 129, 0.15)" : "transparent",
color: activeTab === "curio" ? "var(--accent-emerald)" : "var(--text-secondary)",
fontWeight: activeTab === "curio" ? "600" : "500",
transition: "all 0.2s"
}}
>
<HeartPulse size={18} />
Clinical Workspace
</button>
<button
onClick={() => setActiveTab("logs")}
style={{
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
background: activeTab === "logs" ? "rgba(255, 255, 255, 0.05)" : "transparent",
color: activeTab === "logs" ? "var(--text-primary)" : "var(--text-secondary)",
fontWeight: activeTab === "logs" ? "600" : "500",
transition: "all 0.2s"
}}
>
<Layers size={18} />
Workflows & Logs
</button>
</nav>
</div>
{/* Footer info */}
<div style={{ display: "flex", flexDirection: "column", gap: "8px", borderTop: "1px solid var(--border-color)", paddingTop: "16px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ width: "8px", height: "8px", borderRadius: "50%" }} className="pulse-emerald"></span>
<span style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Temporal Worker Connected</span>
</div>
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>v1.2.0-Production</span>
</div>
</aside>
{/* Main Content Area */}
<main className="main-content">
<header className="header">
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<h1 style={{ fontSize: "18px", textTransform: "capitalize" }}>{activeTab.replace("curio", "Clinical Curio").replace("ops", "OpsAgent Hub")}</h1>
<div style={{ fontSize: "12px", color: "var(--text-muted)", padding: "2px 8px", background: "rgba(255,255,255,0.05)", borderRadius: "4px" }}>
Namespace: ai-agents
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end" }}>
<span style={{ fontSize: "12px", color: "var(--text-primary)" }}>Sysadmin Console</span>
<span style={{ fontSize: "10px", color: "var(--accent-cyan)" }}>192.168.8.250</span>
</div>
<div style={{ width: "36px", height: "36px", borderRadius: "50%", background: "#1f293d", display: "flex", alignItems: "center", justifyContent: "center" }}>
<User size={18} style={{ color: "var(--text-secondary)" }} />
</div>
</div>
</header>
<div className="content-body">
{/* TAB 1: DASHBOARD HOME */}
{activeTab === "dashboard" && (
<div className="fade-in" style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
{/* Stat Cards */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: "20px" }}>
<div className="glass glass-interactive" style={{ padding: "20px" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Total Active Agents</span>
<Cpu style={{ color: "var(--accent-violet)" }} size={20} />
</div>
<h2 style={{ fontSize: "28px" }}>5 / 8</h2>
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginTop: "6px" }}>Active in namespace</div>
</div>
<div className="glass glass-interactive" style={{ padding: "20px" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Temporal Workflows</span>
<RefreshCw style={{ color: "var(--accent-cyan)" }} size={20} />
</div>
<h2 style={{ fontSize: "28px" }}>12 Active</h2>
<div style={{ fontSize: "12px", color: "var(--text-emerald)", marginTop: "6px" }}>98.8% success SLA</div>
</div>
<div className="glass glass-interactive" style={{ padding: "20px" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Diagnostic Logs</span>
<Database style={{ color: "var(--accent-emerald)" }} size={20} />
</div>
<h2 style={{ fontSize: "28px" }}>142 MB</h2>
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginTop: "6px" }}>Persisted in PostgreSQL</div>
</div>
<div className="glass glass-interactive" style={{ padding: "20px" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Action Approvals</span>
<ShieldAlert style={{ color: "var(--accent-amber)" }} size={20} />
</div>
<h2 style={{ fontSize: "28px", color: "var(--accent-amber)" }}>{pendingApprovals.length} Awaiting</h2>
<div style={{ fontSize: "12px", color: "var(--accent-rose)", marginTop: "6px" }}>Require administrator override</div>
</div>
</div>
{/* Agent Status Panel */}
<div className="glass" style={{ padding: "24px" }}>
<h3 style={{ fontSize: "16px", marginBottom: "16px" }}>Core Agent Ecosystem Status</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{[
{ name: "Bernard", role: "GitOps Reviewer & Workflow Monitor", status: "Idle", desc: "Awaiting PR push events on Gitea hooks.", type: "system" },
{ name: "Gumbo", role: "Documentation Summarizer", status: "Active", desc: "Indexing recent technical wiki docs via MCP filesystem.", type: "assistant" },
{ name: "OpsAgent", role: "SRE Diagnostics & Self-Healing", status: "Pending approval", desc: "Awaiting administrator decision for remediation action.", type: "remediator" },
{ name: "Claw", role: "Local Assistant CLI (OpenClaw clone)", status: "Active", desc: "Awaiting operator prompt input.", type: "assistant" },
{ name: "Curio Billing/Triage", role: "HMS AI Assist", status: "Idle", desc: "Standing by for clinical intake recording inputs.", type: "clinical" },
].map((agent, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
<div>
<h4 style={{ fontSize: "14px", fontWeight: "600" }}>{agent.name}</h4>
<p style={{ fontSize: "12px", color: "var(--text-secondary)" }}>{agent.role} <span style={{ color: "var(--text-muted)" }}>{agent.desc}</span></p>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ width: "8px", height: "8px", borderRadius: "50%" }} className={agent.status.includes("Pending") ? "pulse-amber" : "pulse-emerald"}></span>
<span style={{ fontSize: "12px", fontWeight: "600", color: agent.status.includes("Pending") ? "var(--accent-amber)" : "var(--text-secondary)" }}>{agent.status}</span>
</div>
</div>
))}
</div>
</div>
{/* Quick Trigger Block */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
<div className="glass" style={{ padding: "20px" }}>
<h3 style={{ fontSize: "14px", marginBottom: "12px" }}>Diagnostics & Ops Triggers</h3>
<div style={{ display: "flex", gap: "10px" }}>
<button
onClick={() => {
setActiveTab("ops");
}}
style={{ padding: "10px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-amber), #d97706)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
>
<ShieldAlert size={16} />
Go to Approval Center
</button>
<button
onClick={() => {
setActiveTab("claw");
setMessages(prev => [...prev, { role: "assistant", content: "I am ready. Tell me to `check k8s pods` to test system diagnostic logic.", timestamp: "Just now" }]);
}}
style={{ padding: "10px 16px", border: "1px solid var(--border-color)", borderRadius: "6px", background: "rgba(255,255,255,0.05)", color: "var(--text-primary)", fontWeight: "600", cursor: "pointer" }}
>
Diagnose Pods
</button>
</div>
</div>
<div className="glass" style={{ padding: "20px" }}>
<h3 style={{ fontSize: "14px", marginBottom: "12px" }}>Knowledge Intake Trigger</h3>
<div style={{ display: "flex", gap: "10px" }}>
<button
onClick={() => {
alert("Triggered GumboSummarizeWorkflow for api-spec.md");
}}
style={{ padding: "10px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-violet), var(--accent-cyan))", color: "#fff", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
>
<Sparkles size={16} />
Summarize Documentation (Gumbo)
</button>
</div>
</div>
</div>
</div>
)}
{/* TAB 2: CLAW PERSONAL ASSISTANT */}
{activeTab === "claw" && (
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 340px", gap: "24px", height: "calc(100vh - 140px)", overflow: "hidden" }}>
{/* Chat Panel */}
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
{/* Chat Header */}
<div style={{ padding: "16px 24px", borderBottom: "1px solid var(--border-color)", display: "flex", alignItems: "center", gap: "12px", background: "rgba(255,255,255,0.01)" }}>
<div style={{ width: "10px", height: "10px", borderRadius: "50%" }} className="pulse-emerald"></div>
<div>
<h3 style={{ fontSize: "15px" }}>Claw Personal Assistant</h3>
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>Runs locally. Accesses file tools & system terminal.</span>
</div>
</div>
{/* Chat Messages */}
<div style={{ flex: 1, padding: "24px", overflowY: "auto", display: "flex", flexDirection: "column", gap: "16px" }}>
{messages.map((msg, i) => (
<div
key={i}
style={{
display: "flex",
flexDirection: "column",
alignItems: msg.role === "user" ? "flex-end" : "flex-start",
maxWidth: "85%",
alignSelf: msg.role === "user" ? "flex-end" : "flex-start"
}}
>
<div
style={{
padding: "12px 16px",
borderRadius: "12px",
fontSize: "14px",
lineHeight: "1.5",
background: msg.role === "user" ? "var(--accent-cyan)" : "rgba(255, 255, 255, 0.04)",
color: msg.role === "user" ? "#000" : "var(--text-primary)",
border: msg.role === "user" ? "none" : "1px solid var(--border-color)"
}}
>
{msg.content}
</div>
<span style={{ fontSize: "10px", color: "var(--text-muted)", marginTop: "4px", padding: "0 4px" }}>{msg.timestamp}</span>
</div>
))}
{isClawTyping && (
<div style={{ alignSelf: "flex-start", display: "flex", alignItems: "center", gap: "4px", padding: "12px 16px", background: "rgba(255, 255, 255, 0.04)", borderRadius: "12px", border: "1px solid var(--border-color)" }}>
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both" }}></span>
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.2s" }}></span>
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.4s" }}></span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Chat Input */}
<form onSubmit={handleSendMessage} style={{ padding: "20px", borderTop: "1px solid var(--border-color)", display: "flex", gap: "10px", background: "rgba(8,10,16,0.3)" }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Ask Claw: e.g. 'check pods' or 'check disk VMID 102'..."
style={{ flex: 1, padding: "12px 16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "var(--bg-secondary)", color: "var(--text-primary)", outline: "none", fontSize: "14px" }}
/>
<button
type="submit"
style={{ padding: "12px 20px", border: "none", borderRadius: "8px", background: "var(--accent-cyan)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
>
<Send size={16} />
Send
</button>
</form>
</div>
{/* Claw Tool & Console Log Panel */}
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden", borderLeft: "1px solid var(--border-color)" }}>
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)" }}>
<h3 style={{ fontSize: "14px", display: "flex", alignItems: "center", gap: "8px" }}>
<Terminal size={16} style={{ color: "var(--accent-cyan)" }} />
Claw Tool Console
</h3>
</div>
<div style={{ flex: 1, padding: "16px", background: "#05070a", fontFamily: "var(--font-mono)", fontSize: "11px", color: "#a5f3fc", overflowY: "auto", display: "flex", flexDirection: "column", gap: "8px" }}>
{clawTerminalLogs.length === 0 ? (
<div style={{ color: "var(--text-muted)", fontStyle: "italic" }}>Console logs will stream here as Claw calls CLI and API tools...</div>
) : (
clawTerminalLogs.map((log, i) => (
<div key={i} style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
<span style={{ color: "var(--text-muted)" }}>&gt;</span> {log}
</div>
))
)}
<div ref={terminalEndRef} />
</div>
</div>
</div>
)}
{/* TAB 3: OPSAGENT HUB */}
{activeTab === "ops" && (
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 400px", gap: "24px", height: "calc(100vh - 140px)" }}>
{/* Left Column: Diagnostics and Approvals */}
<div style={{ display: "flex", flexDirection: "column", gap: "20px", overflowY: "auto" }}>
{/* Pending Actions */}
<div className="glass" style={{ padding: "24px" }}>
<h2 style={{ fontSize: "16px", marginBottom: "16px", display: "flex", alignItems: "center", gap: "8px" }}>
<ShieldAlert size={20} style={{ color: "var(--accent-amber)" }} />
Awaiting System Approvals (Approval Gate)
</h2>
{pendingApprovals.length === 0 ? (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "40px", gap: "12px", border: "1px dashed var(--border-color)", borderRadius: "8px" }}>
<CheckCircle size={32} style={{ color: "var(--accent-emerald)" }} />
<h4 style={{ color: "var(--text-primary)" }}>No approvals pending</h4>
<p style={{ fontSize: "12px", color: "var(--text-muted)" }}>All systems are running clean and within normal parameters.</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
{pendingApprovals.map((req) => (
<div key={req.id} style={{ border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.02)", overflow: "hidden" }}>
<div style={{ padding: "16px", borderBottom: "1px solid var(--border-color)", display: "flex", justifyContent: "space-between", alignItems: "center", background: "rgba(245,158,11,0.03)" }}>
<div>
<span style={{ fontSize: "11px", fontWeight: "700", textTransform: "uppercase", background: "rgba(245,158,11,0.15)", color: "var(--accent-amber)", padding: "2px 6px", borderRadius: "4px", marginRight: "8px" }}>
{req.action.replace("_", " ")}
</span>
<span style={{ fontSize: "13px", color: "var(--text-primary)", fontWeight: "600" }}>Target: {req.targetType.toUpperCase()} ({req.targetId})</span>
</div>
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>{req.timestamp}</span>
</div>
<div style={{ padding: "16px" }}>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", lineHeight: "1.5", marginBottom: "16px" }}>{req.details}</p>
<div style={{ display: "flex", gap: "10px", justifyContent: "flex-end" }}>
<button
onClick={() => handleReject(req.id)}
style={{ padding: "8px 16px", border: "1px solid var(--accent-rose)", borderRadius: "6px", background: "transparent", color: "var(--accent-rose)", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "6px" }}
>
<XCircle size={14} />
Reject
</button>
<button
onClick={() => handleApprove(req.id, req.action, req.targetId)}
style={{ padding: "8px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-emerald), #059669)", color: "#fff", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "6px" }}
>
<CheckCircle size={14} />
Approve & Execute
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Diagnostics Panel */}
<div className="glass" style={{ padding: "24px" }}>
<h3 style={{ fontSize: "15px", marginBottom: "12px" }}>OpsAgent Diagnostics Tools</h3>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginBottom: "16px" }}>Manually run automated checks across your container nodes.</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
<div style={{ padding: "12px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
<span style={{ fontSize: "12px", fontWeight: "600" }}>K8s Diagnostic</span>
<button
onClick={() => {
setOpsLogs(prev => [...prev, `[Manual Trigger] Initiated Kubernetes diagnostics check for namespace 'default'...`]);
setTimeout(() => {
setOpsLogs(prev => [...prev, `[OpsAgent] web-pod-xyz logs scanned. DB connection failed.`]);
}, 1000);
}}
style={{ width: "100%", padding: "8px", border: "none", borderRadius: "4px", background: "rgba(6, 182, 212, 0.15)", color: "var(--accent-cyan)", fontWeight: "600", fontSize: "11px", cursor: "pointer", marginTop: "8px" }}
>
Scan Namespace
</button>
</div>
<div style={{ padding: "12px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
<span style={{ fontSize: "12px", fontWeight: "600" }}>Proxmox Auditor</span>
<button
onClick={() => {
setOpsLogs(prev => [...prev, `[Manual Trigger] Querying Proxmox host 192.168.8.225 for LXC disk metrics...`]);
setTimeout(() => {
setOpsLogs(prev => [...prev, `[OpsAgent] VMID 102 checked: 94% storage load. VMID 105 checked: 52% load.`]);
}, 1000);
}}
style={{ width: "100%", padding: "8px", border: "none", borderRadius: "4px", background: "rgba(139, 92, 246, 0.15)", color: "var(--accent-violet)", fontWeight: "600", fontSize: "11px", cursor: "pointer", marginTop: "8px" }}
>
Audit Disk Usage
</button>
</div>
</div>
</div>
</div>
{/* Right Column: Execution Live Terminal */}
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)" }}>
<h3 style={{ fontSize: "14px" }}>OpsAgent Execution Terminal Logs</h3>
</div>
<div style={{ flex: 1, padding: "16px", background: "#05070a", fontFamily: "var(--font-mono)", fontSize: "11px", color: "#a3e635", overflowY: "auto", display: "flex", flexDirection: "column", gap: "8px" }}>
{opsLogs.map((log, i) => (
<div key={i} style={{ lineBreak: "anywhere" }}>{log}</div>
))}
</div>
</div>
</div>
)}
{/* TAB 4: CLINICAL WORKSPACE */}
{activeTab === "curio" && (
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "24px", height: "calc(100vh - 140px)" }}>
{/* Doctor Dictation Section */}
<div className="glass" style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "16px", height: "100%", overflowY: "auto" }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<HeartPulse style={{ color: "var(--accent-emerald)" }} size={24} />
<div>
<h2 style={{ fontSize: "16px" }}>Clinical Scribe Workspace</h2>
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>Uses ClinicalIntakeWorkflow (Temporal Task Queue)</span>
</div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<label style={{ fontSize: "12px", color: "var(--text-secondary)", fontWeight: "600" }}>Raw Dictation Input</label>
<textarea
value={dictationText}
onChange={(e) => setDictationText(e.target.value)}
style={{ width: "100%", height: "200px", padding: "16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "var(--bg-secondary)", color: "var(--text-primary)", outline: "none", resize: "none", fontSize: "14px", lineHeight: "1.5" }}
/>
</div>
<button
onClick={handleProcessClinical}
disabled={isProcessingClinical || !dictationText}
style={{ width: "100%", padding: "12px", border: "none", borderRadius: "8px", background: "var(--accent-emerald)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }}
>
{isProcessingClinical ? (
<>
<RefreshCw size={16} className="spin" style={{ animation: "spin 2s linear infinite" }} />
Structuring Dictation via LLM...
</>
) : (
<>
<Play size={16} />
Submit to ClinicalIntake Workflow
</>
)}
</button>
</div>
{/* Structuring Result Output */}
<div className="glass" style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "16px", height: "100%", overflowY: "auto" }}>
<h3 style={{ fontSize: "15px", borderBottom: "1px solid var(--border-color)", paddingBottom: "12px" }}>Structured Output (SOAP Note)</h3>
{clinicalNote ? (
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: "12px" }}>
<div>
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>SUBJECTIVE</span>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.subjective}</p>
</div>
<div>
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>OBJECTIVE</span>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.objective}</p>
</div>
<div>
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>ASSESSMENT</span>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.assessment}</p>
</div>
<div>
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>PLAN</span>
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.plan}</p>
</div>
</div>
<div style={{ borderTop: "1px solid var(--border-color)", paddingTop: "16px" }}>
<h4 style={{ fontSize: "12px", color: "var(--text-primary)", marginBottom: "8px" }}>Generated Billing Codes (ICD-10 / CPT)</h4>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: "12px" }}>
<thead>
<tr style={{ textAlign: "left", color: "var(--text-muted)", borderBottom: "1px solid var(--border-color)" }}>
<th style={{ padding: "8px" }}>Code</th>
<th style={{ padding: "8px" }}>Description</th>
</tr>
</thead>
<tbody>
{billingCodes.map((bc, idx) => (
<tr key={idx} style={{ borderBottom: "1px solid rgba(255,255,255,0.02)" }}>
<td style={{ padding: "8px", fontWeight: "600", color: "var(--accent-cyan)" }}>{bc.code}</td>
<td style={{ padding: "8px", color: "var(--text-secondary)" }}>{bc.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div style={{ display: "flex", flex: 1, alignItems: "center", justifyContent: "center", color: "var(--text-muted)", fontSize: "13px", fontStyle: "italic", border: "1px dashed var(--border-color)", borderRadius: "8px" }}>
No workflow submitted yet. Enter dictation and click submit.
</div>
)}
</div>
</div>
)}
{/* TAB 5: WORKFLOW MONITOR */}
{activeTab === "logs" && (
<div className="fade-in" style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div className="glass" style={{ padding: "24px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
<div>
<h3 style={{ fontSize: "16px" }}>Durable Execution Logs</h3>
<p style={{ fontSize: "12px", color: "var(--text-muted)" }}>Temporal workflows audit records inside the local postgres cluster.</p>
</div>
<button
onClick={() => {
setWorkflows(prev => [
...prev,
{ id: `wf-manual-${Math.floor(Math.random()*900+100)}`, name: "GumboSummarizeWorkflow", agent: "Gumbo", target: "wiki-summary.txt", status: "completed", result: "Completed" }
]);
}}
style={{ padding: "8px 14px", border: "none", borderRadius: "6px", background: "rgba(255,255,255,0.05)", color: "var(--text-primary)", fontWeight: "600", cursor: "pointer", fontSize: "12px" }}
>
Refresh List
</button>
</div>
<div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: "13px" }}>
<thead>
<tr style={{ borderBottom: "1px solid var(--border-color)", color: "var(--text-muted)", textAlign: "left" }}>
<th style={{ padding: "12px 16px" }}>Workflow ID</th>
<th style={{ padding: "12px 16px" }}>Workflow Name</th>
<th style={{ padding: "12px 16px" }}>Assigned Agent</th>
<th style={{ padding: "12px 16px" }}>Target</th>
<th style={{ padding: "12px 16px" }}>Status</th>
<th style={{ padding: "12px 16px" }}>Result</th>
</tr>
</thead>
<tbody>
{workflows.map((wf) => (
<tr key={wf.id} style={{ borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.005)" }}>
<td style={{ padding: "12px 16px", fontFamily: "var(--font-mono)", fontSize: "12px" }}>{wf.id}</td>
<td style={{ padding: "12px 16px", fontWeight: "600" }}>{wf.name}</td>
<td style={{ padding: "12px 16px" }}>{wf.agent}</td>
<td style={{ padding: "12px 16px", color: "var(--text-secondary)" }}>{wf.target}</td>
<td style={{ padding: "12px 16px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<span style={{ width: "6px", height: "6px", borderRadius: "50%" }} className={wf.status === "running" ? "pulse-amber" : "pulse-emerald"}></span>
<span style={{ textTransform: "capitalize", fontWeight: "600", color: wf.status === "running" ? "var(--accent-amber)" : "var(--text-secondary)" }}>
{wf.status}
</span>
</div>
</td>
<td style={{ padding: "12px 16px", color: wf.status === "running" ? "var(--text-muted)" : "var(--text-primary)" }}>{wf.result}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
</main>
</div>
);
}

437
interface/hq-dashboard/package-lock.json generated Normal file
View File

@ -0,0 +1,437 @@
{
"name": "agentic-os-hq-dashboard",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentic-os-hq-dashboard",
"dependencies": {
"lucide-react": "^0.300.0",
"next": "14.2.18",
"react": "18.3.1",
"react-dom": "18.3.1"
}
},
"node_modules/@next/env": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz",
"integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz",
"integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz",
"integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz",
"integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz",
"integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz",
"integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz",
"integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz",
"integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz",
"integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz",
"integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lucide-react": {
"version": "0.300.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.300.0.tgz",
"integrity": "sha512-rQxUUCmWAvNLoAsMZ5j04b2+OJv6UuNLYMY7VK0eVlm4aTwUEjEEHc09/DipkNIlhXUSDn2xoyIzVT0uh7dRsg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "14.2.18",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz",
"integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==",
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.18",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.18",
"@next/swc-darwin-x64": "14.2.18",
"@next/swc-linux-arm64-gnu": "14.2.18",
"@next/swc-linux-arm64-musl": "14.2.18",
"@next/swc-linux-x64-gnu": "14.2.18",
"@next/swc-linux-x64-musl": "14.2.18",
"@next/swc-win32-arm64-msvc": "14.2.18",
"@next/swc-win32-ia32-msvc": "14.2.18",
"@next/swc-win32-x64-msvc": "14.2.18"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}
}
}

View File

@ -9,6 +9,7 @@
"dependencies": {
"next": "14.2.18",
"react": "18.3.1",
"react-dom": "18.3.1"
"react-dom": "18.3.1",
"lucide-react": "^0.300.0"
}
}

View File

@ -0,0 +1,59 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: hq-dashboard
namespace: ai-agents
labels:
app: hq-dashboard
spec:
replicas: 1
selector:
matchLabels:
app: hq-dashboard
template:
metadata:
labels:
app: hq-dashboard
spec:
serviceAccountName: default
initContainers:
- name: git-clone
image: alpine/git
command:
- "sh"
- "-c"
- "git clone http://192.168.8.248:3000/deepkoluguri/agentic-os.git /workspace && cp -r /workspace/interface/hq-dashboard/* /app/"
volumeMounts:
- name: app-code
mountPath: /app
containers:
- name: hq-dashboard
image: node:18-alpine
command: ["sh", "-c", "cd /app && npm install && npm run build && npm start"]
volumeMounts:
- name: app-code
mountPath: /app
ports:
- containerPort: 3000
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: app-code
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: hq-dashboard
namespace: ai-agents
spec:
ports:
- port: 80
targetPort: 3000
selector:
app: hq-dashboard

View File

@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hq-dashboard-ingress
namespace: ai-agents
annotations:
k8s.apisix.apache.org/enable-websocket: "true"
spec:
ingressClassName: apisix
rules:
- host: hq.applaude.net
http:
paths:
- backend:
service:
name: hq-dashboard
port:
number: 80
path: /
pathType: Prefix

View File

@ -0,0 +1,14 @@
{
"name": "whatsapp-gateway",
"version": "1.0.0",
"description": "Local HTTP WhatsApp Web gateway for autonomous alerting",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2",
"qrcode-terminal": "^0.12.0",
"whatsapp-web.js": "^1.23.0"
}
}

115
whatsapp-gateway/server.js Normal file
View File

@ -0,0 +1,115 @@
const express = require('express');
const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');
const app = express();
const PORT = process.env.PORT || 5001;
app.use(express.json());
// Initialize WhatsApp Web Client with LocalAuth to persist session
const client = new Client({
authStrategy: new LocalAuth({
dataPath: './.wwebjs_auth'
}),
puppeteer: {
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
}
});
let isReady = false;
// Event: QR Code generation
client.on('qr', (qr) => {
console.log('\n==================================================================');
console.log('SCAN THIS QR CODE WITH YOUR WHATSAPP APP TO LOG IN:');
console.log('==================================================================\n');
qrcode.generate(qr, { small: true });
console.log('\n==================================================================');
});
// Event: Successfully logged in & authenticated
client.on('ready', () => {
console.log('\n[WhatsApp Bot] Client is ready and authenticated!');
isReady = true;
});
client.on('auth_failure', (msg) => {
console.error('[WhatsApp Bot] Authentication failure:', msg);
});
client.on('disconnected', (reason) => {
console.log('[WhatsApp Bot] Client was logged out:', reason);
isReady = false;
});
// HTTP REST Endpoint to send messages
app.post('/send-message', async (req, res) => {
if (!isReady) {
return res.status(503).json({ success: false, error: 'WhatsApp Web client is not ready. Please scan the QR code in the server terminal.' });
}
const { to, message } = req.body;
if (!to || !message) {
return res.status(400).json({ success: false, error: "Missing 'to' or 'message' fields in JSON payload." });
}
try {
let targetJid = to;
// 1. Check if 'to' is a WhatsApp group invite link or invite code
if (to.includes('chat.whatsapp.com') || /^[A-Za-z0-9]{20,24}$/.test(to)) {
let inviteCode = to;
if (to.includes('chat.whatsapp.com/')) {
inviteCode = to.split('chat.whatsapp.com/')[1].split('?')[0].trim();
}
console.log(`[WhatsApp Bot] Attempting to join group via invite code: ${inviteCode}`);
try {
const groupChatId = await client.acceptInvite(inviteCode);
targetJid = groupChatId;
console.log(`[WhatsApp Bot] Successfully joined/resolved group. JID: ${targetJid}`);
} catch (err) {
console.warn(`[WhatsApp Bot] Accept invite failed (bot might already be in the group):`, err.message);
// Fallback: If joining failed, attempt to find by code if we can't join
return res.status(400).json({ success: false, error: `Failed to join group via invite code: ${err.message}` });
}
}
// 2. Format phone number to standard JID format if it is a plain phone number
if (!targetJid.includes('@')) {
// Strip any prefix, plus, space, parentheses, or dashes
let cleanPhone = targetJid.replace('whatsapp:', '').replace(/[+\s()-]/g, '').trim();
targetJid = `${cleanPhone}@c.us`;
}
console.log(`[WhatsApp Bot] Sending message to target JID: ${targetJid}`);
const response = await client.sendMessage(targetJid, message);
res.json({
success: true,
messageId: response.id._serialized,
recipient: targetJid
});
} catch (error) {
console.error(`[WhatsApp Bot] Failed to send message:`, error);
res.status(500).json({ success: false, error: error.message || error });
}
});
// Health check endpoint
app.get('/status', (req, res) => {
res.json({
service: 'whatsapp-gateway',
ready: isReady,
authenticated: client.info ? true : false
});
});
app.listen(PORT, () => {
console.log(`[WhatsApp Bot Gateway] REST API running at http://localhost:${PORT}`);
});
console.log('[WhatsApp Bot] Initializing client...');
client.initialize();