diff --git a/.agent/workflows/tdd_feature_impl.md b/.agent/workflows/tdd_feature_impl.md
new file mode 100644
index 0000000..2b1ac6a
--- /dev/null
+++ b/.agent/workflows/tdd_feature_impl.md
@@ -0,0 +1,47 @@
+---
+description: [tdd_feature_impl]: How to perform a close-loop check pattern utilizing Production DB locally for Test Driven Development (TDD)
+---
+
+This workflow ensures AI agents implement robust, context-safe features backed by real production data, strictly avoiding regressions.
+
+# Execution Pattern
+
+// turbo
+1. **Sync Production DB to Local**: Execute the pre-configured script to copy the production SQLite database (`192.168.68.113`) to your local workspace.
+```bash
+chmod +x /app/scripts/sync_prod_db.sh && bash /app/scripts/sync_prod_db.sh
+```
+
+2. **Start Local Backend**: Ensure the local backend server is running in the background. Note: Verify if it's already running using `ps aux | grep uvicorn`, otherwise start it.
+```bash
+cd /app/ai-hub && uvicorn app.main:app --host 0.0.0.0 --port 8000 &
+```
+
+3. **Define the Integration Test**: Based on the `to-do` task, write an explicit integration test locally (e.g., in `/tmp/test_feature.py`). Use python `requests` targeting `http://localhost:8000` to simulate an end-to-end client API call testing the *desired* functionality that doesn't exist yet. The test MUST fail initially (Red step of Red-Green-Refactor).
+
+4. **Pause & Switch Sessions (CRITICAL)**: Because iterating on complex implementations causes extreme token bloating and AI context amnesia, you must PAUSE here. Explicitly provide the user with a copy-pasteable prompt template to carry into the new session:
+
+> *"I have synced the production DB and staged the integration test (`/tmp/...`). To prevent cross-impact and memory pollution during execution, **please switch to a new chat session** and copy-paste this prompt:"*
+>
+> ```text
+> Hi! We are working on the Cortex AI-Hub backend (FastAPI + SQLAlchemy) and following a strict TDD close-loop workflow.
+>
+> Please execute **[Task Name]** as defined in the execution plan here: @/app/docs/...
+>
+> 1. I have already synced the production SQLite database locally.
+> 2. An integration test has been staged at `[Test File Path]` which hits `http://localhost:8000/...`.
+> 3. Start the local development server in the background:
+> `cd /app/ai-hub && uvicorn app.main:app --host 0.0.0.0 --port 8000 &`
+> 4. Review the requirements from the execution plan.
+> 5. [Insert specific files to modify here...]
+>
+> You are not finished until you can successfully execute `python3 [Test File Path]` against the local server and it passes all tests automatically. Iterate on your code until it works!
+> ```
+
+5. **Start Implementation (In New Session)**: Once respawned in the new session, begin architectural coding.
+
+6. **Validate & Loop**: Every time you modify the codebase and believe you are "finished", immediately execute the integration test you defined in Step 3.
+```bash
+python3 /tmp/test_feature.py
+```
+If it fails, fix the code natively. You cannot declare the task completed to the user until this script runs cleanly against the local server containing the production DB dump.
diff --git a/ai-hub/app/api/routes/agents.py b/ai-hub/app/api/routes/agents.py
new file mode 100644
index 0000000..1281b78
--- /dev/null
+++ b/ai-hub/app/api/routes/agents.py
@@ -0,0 +1,244 @@
+from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
+from typing import List
+from sqlalchemy.orm import Session
+from app.api.dependencies import ServiceContainer, get_db
+from app.api import schemas
+from app.db.models.agent import AgentTemplate, AgentInstance, AgentTrigger
+from app.db.models import Message
+from app.api.schemas import (
+ AgentTemplateCreate, AgentTemplateResponse,
+ AgentInstanceCreate, AgentInstanceResponse, AgentInstanceStatusUpdate
+)
+import uuid
+import json
+from app.core.orchestration.agent_loop import AgentExecutor
+
+from sqlalchemy.orm import joinedload
+
+def create_agents_router(services: ServiceContainer) -> APIRouter:
+ router = APIRouter()
+
+ @router.get("", response_model=List[AgentInstanceResponse])
+ def get_agents(db: Session = Depends(get_db)):
+ return db.query(AgentInstance).options(joinedload(AgentInstance.template)).all()
+
+ @router.post("/templates", response_model=AgentTemplateResponse)
+ def create_template(request: AgentTemplateCreate, db: Session = Depends(get_db)):
+ template = AgentTemplate(**request.model_dump())
+ db.add(template)
+ db.commit()
+ db.refresh(template)
+ return template
+
+ @router.post("/instances", response_model=AgentInstanceResponse)
+ def create_instance(request: AgentInstanceCreate, db: Session = Depends(get_db)):
+ # Verify template exists
+ template = db.query(AgentTemplate).filter(AgentTemplate.id == request.template_id).first()
+ if not template:
+ raise HTTPException(status_code=404, detail="Template not found")
+
+ instance = AgentInstance(**request.model_dump())
+ db.add(instance)
+ db.commit()
+ db.refresh(instance)
+ return instance
+
+ @router.patch("/{id}/status", response_model=AgentInstanceResponse)
+ def update_status(id: str, request: AgentInstanceStatusUpdate, db: Session = Depends(get_db)):
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+
+ instance.status = request.status
+ db.commit()
+ db.refresh(instance)
+ return instance
+
+ @router.patch("/{id}/config", response_model=AgentInstanceResponse)
+ def update_config(id: str, request: schemas.AgentConfigUpdate, db: Session = Depends(get_db)):
+ from app.db.models.session import Session as SessionModel
+
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+
+ template = db.query(AgentTemplate).filter(AgentTemplate.id == instance.template_id).first()
+
+ if request.name is not None and template:
+ template.name = request.name
+ if request.system_prompt is not None and template:
+ template.system_prompt_path = request.system_prompt
+ if request.max_loop_iterations is not None and template:
+ template.max_loop_iterations = request.max_loop_iterations
+
+ if request.mesh_node_id is not None:
+ instance.mesh_node_id = request.mesh_node_id
+
+ # Update the Session overriding prompt so the running loop picks it up instantly!
+ if instance.session_id:
+ session = db.query(SessionModel).filter(SessionModel.id == instance.session_id).first()
+ if session:
+ if request.system_prompt is not None:
+ session.system_prompt_override = request.system_prompt
+ if hasattr(request, 'provider_name') and request.provider_name is not None:
+ session.provider_name = request.provider_name
+ if request.mesh_node_id is not None:
+ session.attached_node_ids = [request.mesh_node_id] if request.mesh_node_id else []
+
+ db.commit()
+ db.refresh(instance)
+ return instance
+
+ @router.post("/{id}/webhook", status_code=202)
+ def webhook_receiver(id: str, payload: dict, background_tasks: BackgroundTasks, token: str = None, db: Session = Depends(get_db)):
+ # Validate instance
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+
+ # Pass webhook event directly to the Agent Executor to process
+ prompt = f"Webhook Event: {json.dumps(payload)}"
+ background_tasks.add_task(AgentExecutor.run, instance.id, prompt, services.rag_service, services.user_service)
+ return {"message": "Accepted"}
+
+ @router.get("/{id}/triggers", response_model=List[schemas.AgentTriggerResponse])
+ def get_agent_triggers(id: str, db: Session = Depends(get_db)):
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+ return db.query(AgentTrigger).filter(AgentTrigger.instance_id == id).all()
+
+ @router.post("/{id}/triggers", response_model=schemas.AgentTriggerResponse)
+ def create_agent_trigger(id: str, request: schemas.AgentTriggerCreate, db: Session = Depends(get_db)):
+ trigger = AgentTrigger(**request.model_dump())
+ trigger.instance_id = id # Ensure it maps safely
+
+ if trigger.trigger_type == "webhook" and not trigger.webhook_secret:
+ import secrets
+ trigger.webhook_secret = secrets.token_hex(16)
+
+ db.add(trigger)
+ db.commit()
+ db.refresh(trigger)
+ return trigger
+
+ @router.delete("/triggers/{trigger_id}")
+ def delete_agent_trigger(trigger_id: str, db: Session = Depends(get_db)):
+ trigger = db.query(AgentTrigger).filter(AgentTrigger.id == trigger_id).first()
+ if not trigger:
+ raise HTTPException(status_code=404, detail="Trigger not found")
+ db.delete(trigger)
+ db.commit()
+ return {"message": "Trigger deleted successfully"}
+
+
+ @router.get("/{id}/telemetry")
+ def get_telemetry(id: str, db: Session = Depends(get_db)):
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+ # For MVP/Area 3, return mock telemetry data (e.g. baseline or from cgroup)
+ # Real cgroup-based metrics will come in Phase 2
+ return {
+ "cpu_usage": 2.5,
+ "memory_usage": 512,
+ "network_tx": 120,
+ "network_rx": 450
+ }
+
+ @router.get("/{id}/dependencies")
+ def get_dependencies(id: str, db: Session = Depends(get_db)):
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Instance not found")
+ return {
+ "dependencies": [],
+ "edges": []
+ }
+
+ @router.post("/deploy")
+ def deploy_agent(
+ request: schemas.DeployAgentRequest,
+ background_tasks: BackgroundTasks,
+ db: Session = Depends(get_db)
+ ):
+ """
+ One-click agent deployment (Design Doc CUJ 1).
+ Atomically creates: Template → Session → Instance → Locks Session → Injects initial prompt → Starts loop.
+ """
+ from app.db import models as db_models
+
+ # 1. Create Template
+ template = AgentTemplate(
+ name=request.name,
+ description=request.description,
+ system_prompt_path=request.system_prompt,
+ max_loop_iterations=request.max_loop_iterations
+ )
+ db.add(template)
+ db.flush()
+
+ # Resolve default provider mapping if user didn't select one
+ resolved_provider = request.provider_name
+ if not resolved_provider:
+ sys_prefs = services.user_service.get_system_settings(db)
+ resolved_provider = sys_prefs.get('llm', {}).get('default_provider', 'gemini')
+
+ # 2. Create a locked Session for the agent
+ new_session = db_models.Session(
+ user_id="agent-system",
+ provider_name=resolved_provider,
+ feature_name="agent_harness",
+ is_locked=True,
+ system_prompt_override=request.system_prompt
+ )
+ db.add(new_session)
+ db.flush()
+
+ # 3. Create AgentInstance
+ workspace_jail = f"/tmp/cortex/agent_{template.id[:8]}/"
+ instance = AgentInstance(
+ template_id=template.id,
+ session_id=new_session.id,
+ mesh_node_id=request.mesh_node_id,
+ status="idle",
+ current_workspace_jail=workspace_jail
+ )
+ db.add(instance)
+ db.flush()
+
+ # 4. Kick off agent loop if initial prompt was provided
+ # (Message insertion is handled automatically by the RAG service execution)
+ if request.initial_prompt:
+ instance.status = "active"
+ db.commit()
+
+ async def run_wrapper():
+ await AgentExecutor.run(instance.id, request.initial_prompt, services.rag_service, services.user_service)
+
+ background_tasks.add_task(run_wrapper)
+ else:
+ db.commit()
+
+ return {
+ "template_id": template.id,
+ "template_name": template.name,
+ "instance_id": instance.id,
+ "session_id": new_session.id,
+ "status": instance.status,
+ "workspace_jail": workspace_jail,
+ "message": f"Agent '{request.name}' deployed successfully"
+ }
+ @router.delete("/{id}")
+ def delete_agent(id: str, db: Session = Depends(get_db)):
+ from app.db.models.agent import AgentInstance
+ instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
+ if not instance:
+ raise HTTPException(status_code=404, detail="Agent not found")
+
+ # Stop the agent loop if it was active by deleting it (the loop will hit a None instance and return)
+ db.delete(instance)
+ db.commit()
+ return {"message": "Agent deleted successfully"}
+
+ return router
diff --git a/ai-hub/app/api/routes/api.py b/ai-hub/app/api/routes/api.py
index 3e27520..7270512 100644
--- a/ai-hub/app/api/routes/api.py
+++ b/ai-hub/app/api/routes/api.py
@@ -31,5 +31,8 @@
router.include_router(create_skills_router(services))
router.include_router(create_agent_update_router())
router.include_router(create_admin_router(), prefix="/admin")
+
+ from .agents import create_agents_router
+ router.include_router(create_agents_router(services), prefix="/agents", tags=["Agents"])
return router
\ No newline at end of file
diff --git a/ai-hub/app/api/routes/sessions.py b/ai-hub/app/api/routes/sessions.py
index 1d6504b..e4ab25c 100644
--- a/ai-hub/app/api/routes/sessions.py
+++ b/ai-hub/app/api/routes/sessions.py
@@ -109,6 +109,9 @@
session = db.query(models.Session).filter(models.Session.id == session_id).first()
if not session:
raise HTTPException(status_code=404, detail="Session not found.")
+
+ if session.is_locked:
+ raise HTTPException(status_code=403, detail="Cannot clear history of a locked session. Unlock it first.")
deleted = db.query(models.Message).filter(models.Message.session_id == session_id).delete()
db.commit()
@@ -198,6 +201,22 @@
if session_update.tts_provider_name is not None:
session.tts_provider_name = session_update.tts_provider_name
+ if session_update.restrict_skills is not None:
+ session.restrict_skills = session_update.restrict_skills
+
+ if session_update.allowed_skill_names is not None:
+ session.allowed_skill_names = session_update.allowed_skill_names
+
+ if session_update.allowed_skill_ids is not None:
+ skills = db.query(models.Skill).filter(models.Skill.id.in_(session_update.allowed_skill_ids)).all()
+ session.skills = skills
+
+ if session_update.system_prompt_override is not None:
+ session.system_prompt_override = session_update.system_prompt_override
+
+ if session_update.is_locked is not None:
+ session.is_locked = session_update.is_locked
+
db.commit()
db.refresh(session)
return session
@@ -212,6 +231,9 @@
session = db.query(models.Session).filter(models.Session.id == session_id).first()
if not session:
raise HTTPException(status_code=404, detail="Session not found.")
+
+ if session.is_locked:
+ raise HTTPException(status_code=403, detail="Cannot delete a locked session. Unlock it first to delete.")
session.is_archived = True
sync_workspace_id = session.sync_workspace_id
@@ -255,7 +277,8 @@
sessions = db.query(models.Session).filter(
models.Session.user_id == user_id,
models.Session.feature_name == feature_name,
- models.Session.is_archived == False
+ models.Session.is_archived == False,
+ models.Session.is_locked == False
).all()
workspaces_to_purge = []
diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py
index a82a82d..eb27e50 100644
--- a/ai-hub/app/api/schemas.py
+++ b/ai-hub/app/api/schemas.py
@@ -217,6 +217,11 @@
provider_name: Optional[str] = None
stt_provider_name: Optional[str] = None
tts_provider_name: Optional[str] = None
+ restrict_skills: Optional[bool] = None
+ allowed_skill_ids: Optional[List[int]] = None
+ allowed_skill_names: Optional[List[str]] = None
+ system_prompt_override: Optional[str] = None
+ is_locked: Optional[bool] = None
class Session(BaseModel):
"""Defines the shape of a session object returned by the API."""
@@ -233,6 +238,12 @@
attached_node_ids: Optional[List[str]] = Field(default_factory=list)
node_sync_status: Optional[dict] = Field(default_factory=dict)
sync_config: Optional[dict] = Field(default_factory=dict)
+
+ restrict_skills: bool = False
+ allowed_skill_names: Optional[List[str]] = Field(default_factory=list)
+ system_prompt_override: Optional[str] = None
+ is_locked: bool = False
+
model_config = ConfigDict(from_attributes=True)
# --- M3: Session Node Attachment Schemas ---
@@ -502,3 +513,74 @@
# Keep backward-compat alias
AgentNodeSummary = AgentNodeUserView
+
+# ---------------------------------------------------------------------------
+# Agent Schemas
+# ---------------------------------------------------------------------------
+
+class AgentTemplateBase(BaseModel):
+ name: str
+ description: Optional[str] = None
+ system_prompt_path: Optional[str] = None
+ max_loop_iterations: int = 20
+
+class AgentTemplateCreate(AgentTemplateBase):
+ pass
+
+class AgentTemplateResponse(AgentTemplateBase):
+ id: str
+ model_config = ConfigDict(from_attributes=True)
+
+
+class AgentInstanceBase(BaseModel):
+ template_id: str
+ session_id: Optional[int] = None
+ mesh_node_id: Optional[str] = None
+ status: str = "idle"
+ current_workspace_jail: Optional[str] = None
+
+class AgentInstanceCreate(AgentInstanceBase):
+ pass
+
+class AgentInstanceStatusUpdate(BaseModel):
+ status: str
+
+class AgentInstanceResponse(AgentInstanceBase):
+ id: str
+ last_heartbeat: Optional[datetime] = None
+ template: Optional[AgentTemplateResponse] = None
+ model_config = ConfigDict(from_attributes=True)
+
+
+class AgentTriggerBase(BaseModel):
+ instance_id: str
+ trigger_type: str
+ cron_expression: Optional[str] = None
+ webhook_secret: Optional[str] = None
+ webhook_mapping_schema: Optional[dict] = None
+
+class AgentTriggerCreate(AgentTriggerBase):
+ pass
+
+class AgentTriggerResponse(AgentTriggerBase):
+ id: str
+ model_config = ConfigDict(from_attributes=True)
+
+
+class DeployAgentRequest(BaseModel):
+ """One-click deployment: creates template + session + instance atomically."""
+ name: str
+ description: Optional[str] = None
+ system_prompt: Optional[str] = None
+ mesh_node_id: Optional[str] = None
+ max_loop_iterations: int = 20
+ initial_prompt: Optional[str] = None # First message to kick off the loop
+ provider_name: Optional[str] = None
+
+class AgentConfigUpdate(BaseModel):
+ """Day 2 Agent Configuration edits"""
+ name: Optional[str] = None
+ system_prompt: Optional[str] = None
+ max_loop_iterations: Optional[int] = None
+ mesh_node_id: Optional[str] = None
+ provider_name: Optional[str] = None
diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py
new file mode 100644
index 0000000..5e7d94d
--- /dev/null
+++ b/ai-hub/app/core/orchestration/agent_loop.py
@@ -0,0 +1,88 @@
+import asyncio
+import time
+from datetime import datetime
+from sqlalchemy.orm import Session
+from tenacity import retry, wait_exponential, stop_after_attempt
+import json
+
+from app.db.session import SessionLocal
+from app.db.models.agent import AgentInstance, AgentTemplate
+from app.db.models import Message
+
+class AgentExecutor:
+ @staticmethod
+ async def run(agent_id: str, prompt: str, rag_service, user_service):
+ """Asynchronous execution loop for the agent."""
+ # Create a fresh DB session for the background task
+ db: Session = SessionLocal()
+ try:
+ instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first()
+ if not instance or not prompt:
+ return
+
+ # Acquire Lease
+ instance.last_heartbeat = datetime.utcnow()
+ instance.status = "active"
+ db.commit()
+
+ template = db.query(AgentTemplate).filter(AgentTemplate.id == instance.template_id).first()
+ if not template:
+ instance.status = "error_suspended"
+ db.commit()
+ return
+
+ max_iterations = template.max_loop_iterations or 20
+ session_id = instance.session_id
+
+ # Load session to check configured assigned provider
+ from app.db.models.session import Session as SessionModel
+ agent_session = db.query(SessionModel).filter(SessionModel.id == session_id).first()
+
+ provider_name = getattr(agent_session, "provider_name", None)
+
+ # If not explicitly defined on session, fallback to smartest available
+ if not provider_name and user_service:
+ provider_name = "gemini"
+ sys_prefs = user_service.get_system_settings(db)
+ providers = sys_prefs.get("llm", {}).get("providers", {})
+ for best_choice in ["deepseek", "gemini", "openai", "anthropic"]:
+ if best_choice in providers and providers[best_choice].get("api_key"):
+ provider_name = best_choice
+ break
+
+ print(f"[AgentExecutor] Starting run for {agent_id} with provider '{provider_name}'. Prompt length: {len(prompt)}")
+
+ # Iterate the RAG architecture to solve the prompt
+ try:
+ # We consume the generator completely to let it execute all tools and generate reasoning
+ async for event in rag_service.chat_with_rag(
+ db=db,
+ session_id=session_id,
+ prompt=prompt,
+ provider_name=provider_name,
+ load_faiss_retriever=False,
+ user_service=user_service
+ ):
+ # We could log events here if needed
+ pass
+
+ # Execution complete
+ instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first()
+ if instance.status == "active":
+ instance.status = "idle" # Completed work
+ db.commit()
+
+ except Exception as e:
+ print(f"[AgentExecutor] RAG execution failed for {agent_id}: {e}")
+ instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first()
+ instance.status = "error_suspended"
+ db.commit()
+
+ except Exception as e:
+ print(f"[AgentExecutor] Unhandled loop error: {e}")
+ instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first()
+ if instance:
+ instance.status = "error_suspended"
+ db.commit()
+ finally:
+ db.close()
diff --git a/ai-hub/app/core/orchestration/architect.py b/ai-hub/app/core/orchestration/architect.py
index 9fb2f64..8afcf4b 100644
--- a/ai-hub/app/core/orchestration/architect.py
+++ b/ai-hub/app/core/orchestration/architect.py
@@ -33,13 +33,14 @@
sync_workspace_id: Optional[str] = None,
session_id: Optional[int] = None,
feature_name: str = "chat",
- prompt_slug: str = "rag-pipeline"
+ prompt_slug: str = "rag-pipeline",
+ session_override: Optional[str] = None
):
# 1. Initialize Context & Messages
messages = self.memory.prepare_initial_messages(
question, context_chunks, history, feature_name, mesh_context, sync_workspace_id,
db=db, user_id=user_id, prompt_service=prompt_service, prompt_slug=prompt_slug,
- tools=tools
+ tools=tools, session_override=session_override
)
# DEBUG: Log the total prompt size to detect bloated contexts
@@ -75,7 +76,7 @@
if safety.check_cancellation():
yield {"type": "reasoning", "content": "\n> **🛑 User Interruption:** Terminating loop.\n"}
return
- messages = self.memory.compress_history(messages)
+ messages = await self.memory.compress_history(messages, llm_provider)
# B. Turn Start Heartbeat
self._update_turn_marker(messages, turn)
diff --git a/ai-hub/app/core/orchestration/memory.py b/ai-hub/app/core/orchestration/memory.py
index d1ddf10..517e958 100644
--- a/ai-hub/app/core/orchestration/memory.py
+++ b/ai-hub/app/core/orchestration/memory.py
@@ -16,7 +16,8 @@
feature_name: str, mesh_context: str, sync_workspace_id: Optional[str],
db: Optional[Session] = None, user_id: Optional[str] = None,
prompt_service: Any = None, prompt_slug: str = "rag-pipeline",
- tools: List[Dict[str, Any]] = None) -> List[Dict[str, str]]:
+ tools: List[Dict[str, Any]] = None,
+ session_override: Optional[str] = None) -> List[Dict[str, str]]:
from .profiles import get_profile
profile = get_profile(feature_name)
@@ -31,6 +32,10 @@
db_prompt = prompt_service.get_prompt_by_slug(db, prompt_slug, user_id)
if db_prompt:
template = db_prompt.content
+
+ # Session Specific Override takes maximum priority
+ if session_override and session_override.strip():
+ template = session_override
# Augmented mesh info
mesh_info = mesh_context
@@ -77,12 +82,12 @@
{"role": "user", "content": question}
]
- def compress_history(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- """Summarizes middle turns to preserve context window."""
+ async def compress_history(self, messages: List[Dict[str, Any]], llm_provider=None) -> List[Dict[str, Any]]:
+ """Summarizes middle turns using the LLM to preserve context window while preventing amnesia."""
if len(messages) < 60:
return messages
- logging.info(f"[ContextManager] Memory overflow ({len(messages)}). Compressing middle history...")
+ logging.info(f"[ContextManager] Memory overflow ({len(messages)}). Compressing middle history via LLM...")
system_msg = messages[0]
pivot_idx = len(messages) - 20
while pivot_idx > 1 and messages[pivot_idx].get("role") != "user":
@@ -106,9 +111,32 @@
summary_text = "\n".join(summary_lines)
+ # If we have an LLM provider, do a fast, dense compression
+ if llm_provider:
+ try:
+ prompt = f"Please read the following timeline of chronological actions and compress them into a dense, highly factual summary paragraph. Retain critical paths, file names, errors, and system state outcomes. Omit pleasantries and reasoning fluff.\n\n{summary_text}"
+
+ comp_res = await llm_provider.acompletion(
+ messages=[{"role": "user", "content": prompt}],
+ max_tokens=500,
+ temperature=0.0,
+ stream=False
+ )
+ if comp_res and comp_res.choices:
+ try:
+ llm_summary = comp_res.choices[0].message.content
+ except AttributeError:
+ # Fallback for weird dictionary responses
+ llm_summary = comp_res.choices[0].get("message", {}).get("content")
+
+ if llm_summary:
+ summary_text = llm_summary.strip()
+ except Exception as e:
+ logging.warning(f"LLM Summary compression failed, falling back to python truncation: {e}")
+
history_msg = {
"role": "user",
- "content": f"[SYSTEM: Historical Context Summary]\n\n{summary_text}\n\n[End Summary.]"
+ "content": f"[SYSTEM: Historical Context Timeline Condensed into Summary]\n\n{summary_text}\n\n[End Summary.]"
}
ack_msg = {
"role": "assistant",
diff --git a/ai-hub/app/core/services/rag.py b/ai-hub/app/core/services/rag.py
index b7c01b1..84d4d08 100644
--- a/ai-hub/app/core/services/rag.py
+++ b/ai-hub/app/core/services/rag.py
@@ -95,7 +95,7 @@
tools = []
if self.tool_service:
- tools = self.tool_service.get_available_tools(db, session.user_id, feature=session.feature_name)
+ tools = self.tool_service.get_available_tools(db, session.user_id, feature=session.feature_name, session_id=session.id)
profile = get_profile(session.feature_name)
mesh_context = ""
@@ -194,7 +194,8 @@
sync_workspace_id = session.sync_workspace_id,
session_id = session_id,
feature_name = session.feature_name,
- prompt_slug = profile.default_prompt_slug
+ prompt_slug = profile.default_prompt_slug,
+ session_override = session.system_prompt_override
):
if event["type"] == "content":
full_answer += event["content"]
diff --git a/ai-hub/app/core/services/tool.py b/ai-hub/app/core/services/tool.py
index bdd6276..a347cf8 100644
--- a/ai-hub/app/core/services/tool.py
+++ b/ai-hub/app/core/services/tool.py
@@ -22,16 +22,28 @@
self._local_skills = {s.name: s for s in local_skills}
tool_registry.load_plugins()
- def get_available_tools(self, db: Session, user_id: str, feature: str = None) -> List[Dict[str, Any]]:
+ def get_available_tools(self, db: Session, user_id: str, feature: str = None, session_id: int = None) -> List[Dict[str, Any]]:
"""
Retrieves all tools the user is authorized to use, optionally filtered by feature.
"""
+ allowed_skill_names = None
+ if session_id and db:
+ session_obj = db.query(models.Session).filter(models.Session.id == session_id).first()
+ if session_obj and getattr(session_obj, "restrict_skills", False):
+ allowed_skill_names = set()
+ if session_obj.allowed_skill_names:
+ allowed_skill_names.update(session_obj.allowed_skill_names)
+ if getattr(session_obj, "skills", None):
+ allowed_skill_names.update(s.name for s in session_obj.skills)
+
# 1. Fetch system/local skills and filter by feature if requested
local_skills = self._local_skills.values()
if feature:
local_skills = [s for s in local_skills if feature in getattr(s, "features", ["chat"])]
tools = [s.to_tool_definition() for s in local_skills]
+ if allowed_skill_names is not None:
+ tools = [t for t in tools if t["function"]["name"] in allowed_skill_names]
# 2. Add FS-defined skills (System skills or user-owned)
from app.core.skills.fs_loader import fs_loader
@@ -51,6 +63,8 @@
fs_skill["files"] = [_DictObj(f) for f in fs_skill.get("files", [])]
db_skills.append(_DictObj(fs_skill))
+ if allowed_skill_names is not None:
+ db_skills = [ds for ds in db_skills if ds.name in allowed_skill_names]
import litellm
max_md_len = 1000
diff --git a/ai-hub/app/db/__init__.py b/ai-hub/app/db/__init__.py
index e69de29..b03d5b4 100644
--- a/ai-hub/app/db/__init__.py
+++ b/ai-hub/app/db/__init__.py
@@ -0,0 +1 @@
+from .models.agent import AgentTemplate, AgentInstance, AgentTrigger
\ No newline at end of file
diff --git a/ai-hub/app/db/migrate.py b/ai-hub/app/db/migrate.py
index ac6fb85..69f34fa 100644
--- a/ai-hub/app/db/migrate.py
+++ b/ai-hub/app/db/migrate.py
@@ -52,6 +52,10 @@
("node_sync_status", "TEXT"),
("sync_config", "TEXT"),
("is_cancelled", "INTEGER DEFAULT 0"),
+ ("restrict_skills", "BOOLEAN DEFAULT 0"),
+ ("allowed_skill_names","TEXT"),
+ ("system_prompt_override","TEXT"),
+ ("is_locked", "BOOLEAN DEFAULT 0"),
]
for col_name, col_type in session_required_columns:
if col_name not in session_columns:
@@ -160,6 +164,22 @@
except Exception as e:
logger.error(f"Failed to create 'skill_group_access': {e}")
+ # Create session_skills table if it doesn't exist
+ if not inspector.has_table("session_skills"):
+ logger.info("Creating table 'session_skills'...")
+ try:
+ conn.execute(text("""
+ CREATE TABLE IF NOT EXISTS session_skills (
+ session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
+ skill_id INTEGER NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
+ PRIMARY KEY (session_id, skill_id)
+ )
+ """))
+ conn.commit()
+ logger.info("Table 'session_skills' created.")
+ except Exception as e:
+ logger.error(f"Failed to create 'session_skills': {e}")
+
# Create skill_files table if it doesn't exist
if not inspector.has_table("skill_files"):
logger.info("Creating table 'skill_files'...")
@@ -214,6 +234,61 @@
except Exception as e:
logger.error(f"Failed to drop column '{col_name}' from 'skills'. SQLite might not support drop column: {e}")
+ # --- Area 1: Agent Infrastructure Tables ---
+ if not inspector.has_table("agent_templates"):
+ logger.info("Creating table 'agent_templates'...")
+ try:
+ conn.execute(text("""
+ CREATE TABLE IF NOT EXISTS agent_templates (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT,
+ system_prompt_path TEXT,
+ max_loop_iterations INTEGER DEFAULT 20
+ )
+ """))
+ conn.commit()
+ logger.info("Table 'agent_templates' created.")
+ except Exception as e:
+ logger.error(f"Failed to create 'agent_templates': {e}")
+
+ if not inspector.has_table("agent_instances"):
+ logger.info("Creating table 'agent_instances'...")
+ try:
+ conn.execute(text("""
+ CREATE TABLE IF NOT EXISTS agent_instances (
+ id TEXT PRIMARY KEY,
+ template_id TEXT NOT NULL REFERENCES agent_templates(id),
+ session_id INTEGER REFERENCES sessions(id),
+ mesh_node_id TEXT,
+ status TEXT DEFAULT 'idle',
+ current_workspace_jail TEXT,
+ last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ """))
+ conn.commit()
+ logger.info("Table 'agent_instances' created.")
+ except Exception as e:
+ logger.error(f"Failed to create 'agent_instances': {e}")
+
+ if not inspector.has_table("agent_triggers"):
+ logger.info("Creating table 'agent_triggers'...")
+ try:
+ conn.execute(text("""
+ CREATE TABLE IF NOT EXISTS agent_triggers (
+ id TEXT PRIMARY KEY,
+ instance_id TEXT NOT NULL REFERENCES agent_instances(id),
+ trigger_type TEXT NOT NULL,
+ cron_expression TEXT,
+ webhook_secret TEXT,
+ webhook_mapping_schema JSON
+ )
+ """))
+ conn.commit()
+ logger.info("Table 'agent_triggers' created.")
+ except Exception as e:
+ logger.error(f"Failed to create 'agent_triggers': {e}")
+
logger.info("Database migrations complete.")
diff --git a/ai-hub/app/db/models/__init__.py b/ai-hub/app/db/models/__init__.py
index 9850d37..a0692a7 100644
--- a/ai-hub/app/db/models/__init__.py
+++ b/ai-hub/app/db/models/__init__.py
@@ -1,3 +1,4 @@
+from app.db.database import Base
from .user import User, Group
from .session import Session, Message
from .document import Document, VectorMetadata
diff --git a/ai-hub/app/db/models/agent.py b/ai-hub/app/db/models/agent.py
new file mode 100644
index 0000000..60c652c
--- /dev/null
+++ b/ai-hub/app/db/models/agent.py
@@ -0,0 +1,42 @@
+from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Enum, JSON, Boolean
+from sqlalchemy.orm import relationship
+import uuid
+import datetime
+from app.db.database import Base
+
+class AgentTemplate(Base):
+ __tablename__ = 'agent_templates'
+
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
+ name = Column(String, nullable=False)
+ description = Column(String, nullable=True)
+ system_prompt_path = Column(String, nullable=True)
+ max_loop_iterations = Column(Integer, default=20)
+
+ instances = relationship("AgentInstance", back_populates="template")
+
+class AgentInstance(Base):
+ __tablename__ = 'agent_instances'
+
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
+ template_id = Column(String, ForeignKey('agent_templates.id'), nullable=False)
+ session_id = Column(Integer, ForeignKey('sessions.id'), nullable=True)
+ mesh_node_id = Column(String, nullable=True) # Just use string or connect to agent_nodes.node_id if needed
+ status = Column(String, default='idle') # Enum: active, idle, listening, error_suspended
+ current_workspace_jail = Column(String, nullable=True)
+ last_heartbeat = Column(DateTime, default=datetime.datetime.utcnow)
+
+ template = relationship("AgentTemplate", back_populates="instances")
+ triggers = relationship("AgentTrigger", back_populates="instance")
+
+class AgentTrigger(Base):
+ __tablename__ = 'agent_triggers'
+
+ id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
+ instance_id = Column(String, ForeignKey('agent_instances.id'), nullable=False)
+ trigger_type = Column(String, nullable=False) # Enum: webhook, cron, manual
+ cron_expression = Column(String, nullable=True)
+ webhook_secret = Column(String, nullable=True)
+ webhook_mapping_schema = Column(JSON, nullable=True)
+
+ instance = relationship("AgentInstance", back_populates="triggers")
diff --git a/ai-hub/app/db/models/session.py b/ai-hub/app/db/models/session.py
index 9e34798..4915f95 100644
--- a/ai-hub/app/db/models/session.py
+++ b/ai-hub/app/db/models/session.py
@@ -1,8 +1,13 @@
from datetime import datetime
-from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON, Table
from sqlalchemy.orm import relationship
from ..database import Base
+session_skills = Table('session_skills', Base.metadata,
+ Column('session_id', Integer, ForeignKey('sessions.id', ondelete="CASCADE"), primary_key=True),
+ Column('skill_id', Integer, ForeignKey('skills.id', ondelete="CASCADE"), primary_key=True)
+)
+
class Session(Base):
__tablename__ = 'sessions'
@@ -21,8 +26,14 @@
attached_node_ids = Column(JSON, default=[], nullable=True)
node_sync_status = Column(JSON, default={}, nullable=True)
sync_config = Column(JSON, default={}, nullable=True)
+
+ restrict_skills = Column(Boolean, default=False, nullable=False)
+ allowed_skill_names = Column(JSON, default=[], nullable=True)
+ system_prompt_override = Column(Text, nullable=True)
+ is_locked = Column(Boolean, default=False, nullable=False)
messages = relationship("Message", back_populates="session", cascade="all, delete-orphan")
+ skills = relationship("Skill", secondary=session_skills, backref="sessions")
user = relationship("User", back_populates="sessions")
def __repr__(self):
diff --git a/ai-hub/docs/api_reference/models.md b/ai-hub/docs/api_reference/models.md
index 75c310b..f8067ae 100644
--- a/ai-hub/docs/api_reference/models.md
+++ b/ai-hub/docs/api_reference/models.md
@@ -509,6 +509,10 @@
| `attached_node_ids` | `anyOf` | |
| `node_sync_status` | `anyOf` | |
| `sync_config` | `anyOf` | |
+| `restrict_skills` | `boolean` | |
+| `allowed_skill_names` | `anyOf` | |
+| `system_prompt_override` | `anyOf` | |
+| `is_locked` | `boolean` | |
## `SessionCreate`
@@ -559,6 +563,11 @@
| `provider_name` | `anyOf` | |
| `stt_provider_name` | `anyOf` | |
| `tts_provider_name` | `anyOf` | |
+| `restrict_skills` | `anyOf` | |
+| `allowed_skill_ids` | `anyOf` | |
+| `allowed_skill_names` | `anyOf` | |
+| `system_prompt_override` | `anyOf` | |
+| `is_locked` | `anyOf` | |
## `SkillConfig`
diff --git a/ai-hub/uvicorn.log b/ai-hub/uvicorn.log
new file mode 100644
index 0000000..3e9d165
--- /dev/null
+++ b/ai-hub/uvicorn.log
@@ -0,0 +1,282 @@
+nohup: ignoring input
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'browser_automation_agent'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_file_explorer'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_inspect_drift'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_sync_control'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_terminal_control'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_wait_tasks'
+INFO:app.core.tools.registry:Registered dynamic tool plugin: 'read_skill_artifact'
+INFO: Started server process [79083]
+INFO: Waiting for application startup.
+INFO:app.db.migrate:Starting database migrations...
+INFO:app.db.migrate:Column 'audio_path' already exists in 'messages'.
+INFO:app.db.migrate:Column 'model_response_time' already exists in 'messages'.
+INFO:app.db.migrate:Column 'token_count' already exists in 'messages'.
+INFO:app.db.migrate:Column 'reasoning_content' already exists in 'messages'.
+INFO:app.db.migrate:Column 'stt_provider_name' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'tts_provider_name' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'sync_workspace_id' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'attached_node_ids' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'node_sync_status' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'sync_config' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'is_cancelled' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'restrict_skills' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'allowed_skill_names' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'system_prompt_override' already exists in 'sessions'.
+INFO:app.db.migrate:Column 'is_locked' already exists in 'sessions'.
+INFO:app.db.migrate:Database migrations complete.
+INFO:app.core.services.node_registry:[NodeRegistry] Reset all DB node statuses to 'offline'.
+ERROR:app.app:[M6] Failed to start gRPC server: No module named 'grpc_reflection'
+INFO:app.core.skills.bootstrap:Checking for system skills bootstrapping...
+INFO:app.core.skills.bootstrap:System skills bootstrap completed.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+✅ Loading configuration from app/config.yaml
+Application startup...
+--- ⚙️ Application Configuration ---
+ - ALLOW_OIDC_LOGIN: False
+ - ALLOW_PASSWORD_LOGIN: ***
+ - DATABASE_URL: sqlite:///./data/ai_hub.db
+ - DATA_DIR: /app/data
+ - DB_MODE: sqlite
+ - DEEPSEEK_API_KEY: sk-a...6bf2
+ - DEEPSEEK_MODEL_NAME: deepseek-chat
+ - EMBEDDING_API_KEY: AIza...sKuI
+ - EMBEDDING_DIMENSION: 768
+ - EMBEDDING_MODEL_NAME: models/text-embedding-004
+ - EMBEDDING_PROVIDER: google_gemini
+ - FAISS_INDEX_PATH: data/faiss_index.bin
+ - GEMINI_API_KEY: AIza...sKuI
+ - GEMINI_MODEL_NAME: gemini-1.5-flash
+ - GRPC_CERT_PATH: None
+ - GRPC_EXTERNAL_ENDPOINT: None
+ - GRPC_KEY_PATH: Not Set
+ - GRPC_TLS_ENABLED: False
+ - LLM_PROVIDERS: {'deepseek': {'api_key': 'sk-a1b3b85a32a942c3b80e06566ef46bf2'}, 'gemini': {'api_key': 'AIzaSyBn5HYiZ8yKmNL0ambyz4Aspr5lKw1sKuI'}, 'openai': {'api_key': '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'}}
+ - LOG_LEVEL: DEBUG
+ - OIDC_CLIENT_ID: cortex-server
+ - OIDC_CLIENT_SECRET: aYc2...leZI
+ - OIDC_ENABLED: False
+ - OIDC_REDIRECT_URI: http://localhost:8001/users/login/callback
+ - OIDC_SERVER_URL: https://auth.jerxie.com
+ - OPENAI_API_KEY: sk-p...h34A
+ - PROJECT_NAME: Cortex Hub
+ - SECRET_KEY: aYc2...leZI
+ - SKILLS_DIR: /app/data/skills
+ - STT_API_KEY: AIza...sKuI
+ - STT_MODEL_NAME: gemini-2.5-flash
+ - STT_PROVIDER: google_gemini
+ - STT_PROVIDERS: {}
+ - SUPER_ADMINS: ['axieyangb@gmail.com']
+ - TTS_API_KEY: AIza...sKuI
+ - TTS_MODEL_NAME: gemini-2.5-flash-preview-tts
+ - TTS_PROVIDER: google_gemini
+ - TTS_PROVIDERS: {}
+ - TTS_VOICE_NAME: Kore
+ - VERSION: 1.0.0
+------------------------------------
+Creating database tables...
+INFO: 127.0.0.1:37766 - "POST /sessions/ HTTP/1.1" 200 OK
+INFO: 127.0.0.1:37782 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK
+INFO: 127.0.0.1:37784 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK
+INFO: 127.0.0.1:37790 - "POST /api/v1/agents/b8bad024-ca14-42a5-baf4-01e9be120f54/webhook HTTP/1.1" 202 Accepted
+INFO: 127.0.0.1:37804 - "GET /api/v1/agents HTTP/1.1" 200 OK
+[AgentExecutor] Agent b8bad024-ca14-42a5-baf4-01e9be120f54 suspended: max iterations reached.
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+ [📁🧹] Running Mirror Cleanup. Active Sessions: 11
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+INFO: 127.0.0.1:38568 - "POST /sessions/ HTTP/1.1" 200 OK
+INFO: 127.0.0.1:38576 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK
+INFO: 127.0.0.1:38578 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK
+INFO: 127.0.0.1:38588 - "GET /api/v1/agents/ac074e05-874e-4173-ab35-20d0edcabc77/telemetry HTTP/1.1" 404 Not Found
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
+INFO: 127.0.0.1:53798 - "POST /sessions/ HTTP/1.1" 200 OK
+INFO: 127.0.0.1:53800 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK
+INFO: 127.0.0.1:53806 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK
+INFO: 127.0.0.1:53814 - "GET /api/v1/agents/bf04a4e8-4099-41fd-b71e-96742eafb55f/telemetry HTTP/1.1" 404 Not Found
+
+==================================================
+📡 CORTEX MESH DASHBOARD | 0 Nodes Online
+--------------------------------------------------
+ No nodes currently connected.
+==================================================
+
diff --git a/curl_setup_agent.sh b/curl_setup_agent.sh
new file mode 100644
index 0000000..d086c2b
--- /dev/null
+++ b/curl_setup_agent.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436"
+API="https://ai.jerxie.com/api/v1"
+
+echo "Creating template..."
+curl -s -X POST "$API/agents/templates/" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "id": "demo-telemetry-agent",
+ "name": "Live Telemetry Demo",
+ "system_prompt_template": "Just a test.",
+ "allowed_skills": [],
+ "resource_limits": {"max_memory_mb": 512, "max_cpu_percent": 80}
+ }'
+
+echo -e "\nCreating session..."
+RESP=$(curl -s -X POST "$API/sessions/" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "user_id": "'"$USER_ID"'",
+ "feature_name": "agent_dashboard"
+ }')
+echo "$RESP"
+
+# Since jq might not be available, extract string matching "id": 123
+SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1)
+
+if [ -z "$SESSION_ID" ]; then
+ echo "Failed to extract session ID."
+ exit 1
+fi
+
+echo -e "\nCreating instance..."
+curl -s -X POST "$API/agents/instances/" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "template_id": "demo-telemetry-agent",
+ "session_id": '"$SESSION_ID"',
+ "mesh_node_id": "test-node-1",
+ "status": "active",
+ "current_workspace_jail": "/tmp/cortex-sync"
+ }'
diff --git a/curl_setup_agent_fix.sh b/curl_setup_agent_fix.sh
new file mode 100644
index 0000000..c72e260
--- /dev/null
+++ b/curl_setup_agent_fix.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436"
+API="https://ai.jerxie.com/api/v1"
+
+echo "Creating template..."
+curl -s -X POST "$API/agents/templates" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "id": "demo-telemetry-agent",
+ "name": "Live Telemetry Demo",
+ "system_prompt_template": "Just a test.",
+ "allowed_skills": [],
+ "resource_limits": {"max_memory_mb": 512, "max_cpu_percent": 80}
+ }'
+
+echo -e "\nCreating session..."
+RESP=$(curl -s -X POST "$API/sessions/" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "user_id": "'"$USER_ID"'",
+ "feature_name": "agent_dashboard"
+ }')
+echo "$RESP"
+
+SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1)
+
+if [ -z "$SESSION_ID" ]; then
+ echo "Failed to extract session ID."
+ exit 1
+fi
+
+echo -e "\nCreating instance..."
+curl -s -X POST "$API/agents/instances" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "template_id": "demo-telemetry-agent",
+ "session_id": '"$SESSION_ID"',
+ "mesh_node_id": "test-node-1",
+ "status": "active",
+ "current_workspace_jail": "/tmp/cortex-sync"
+ }'
diff --git a/curl_setup_agent_fix2.sh b/curl_setup_agent_fix2.sh
new file mode 100644
index 0000000..ef66e49
--- /dev/null
+++ b/curl_setup_agent_fix2.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436"
+API="https://ai.jerxie.com/api/v1"
+
+echo "Creating template..."
+TPL_RESP=$(curl -s -X POST "$API/agents/templates" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Live Telemetry Demo"
+ }')
+echo "$TPL_RESP"
+TPL_ID=$(echo "$TPL_RESP" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -n 1)
+
+echo -e "\nCreating session..."
+RESP=$(curl -s -X POST "$API/sessions/" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "user_id": "'"$USER_ID"'",
+ "feature_name": "agent_dashboard"
+ }')
+echo "$RESP"
+
+SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1)
+
+echo -e "\nCreating instance..."
+curl -s -X POST "$API/agents/instances" \
+ -H "X-User-ID: $USER_ID" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "template_id": "'"$TPL_ID"'",
+ "session_id": '"$SESSION_ID"',
+ "mesh_node_id": "test-node-1",
+ "status": "active",
+ "current_workspace_jail": "/tmp/cortex-sync"
+ }'
diff --git a/deployment/jerxie-prod/docker-compose.production.yml b/deployment/jerxie-prod/docker-compose.production.yml
index ed3330c..93a389e 100644
--- a/deployment/jerxie-prod/docker-compose.production.yml
+++ b/deployment/jerxie-prod/docker-compose.production.yml
@@ -5,6 +5,8 @@
services:
ai-hub:
+ env_file:
+ - .env
environment:
- HUB_PUBLIC_URL=https://ai.jerxie.com
- HUB_GRPC_ENDPOINT=ai.jerxie.com:443
diff --git a/envoy_listener_fix.yaml b/envoy_listener_fix.yaml
new file mode 100644
index 0000000..aa5bdcf
--- /dev/null
+++ b/envoy_listener_fix.yaml
@@ -0,0 +1,59 @@
+'@type': type.googleapis.com/envoy.config.listener.v3.Listener
+address:
+ socketAddress:
+ address: 0.0.0.0
+ portValue: 10001
+filterChains:
+ - filterChainMatch:
+ serverNames:
+ - ai.jerxie.com
+ filters:
+ - name: envoy.filters.network.http_connection_manager
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ httpFilters:
+ - name: envoy.filters.http.router
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ routeConfig:
+ name: ai_unified_service
+ virtualHosts:
+ - domains:
+ - ai.jerxie.com
+ - ai.jerxie.com:443
+ name: ai_service
+ routes:
+ - match:
+ prefix: /agent.
+ route:
+ autoHostRewrite: false
+ cluster: _ai_agent_orchestrator
+ maxStreamDuration:
+ grpcTimeoutHeaderMax: 0s
+ timeout: 0s
+ - match:
+ prefix: /
+ route:
+ cluster: _ai_unified_server
+ timeout: 0s
+ statPrefix: ingress_http
+ upgradeConfigs:
+ - upgradeType: websocket
+ transportSocket:
+ name: envoy.transport_sockets.tls
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
+ commonTlsContext:
+ alpnProtocols:
+ - h2
+ - http/1.1
+ tlsCertificateSdsSecretConfigs:
+ - name: ai_jerxie_com
+ sdsConfig:
+ apiConfigSource:
+ apiType: GRPC
+ grpcServices:
+ - envoyGrpc:
+ clusterName: xds_cluster
+ transportApiVersion: V3
+ resourceApiVersion: V3
diff --git a/envoy_request.json b/envoy_request.json
new file mode 100644
index 0000000..3fc3c15
--- /dev/null
+++ b/envoy_request.json
@@ -0,0 +1 @@
+{"name": "https_listener", "yaml": "'@type': type.googleapis.com/envoy.config.listener.v3.Listener\naddress:\n socketAddress:\n address: 0.0.0.0\n portValue: 10001\nfilterChains:\n - filterChainMatch:\n serverNames:\n - pcb.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n name: pcb_service\n virtualHosts:\n - domains:\n - pcb.jerxie.com\n name: pcb_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _pcb_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: pcb_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - monitor.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - monitor.jerxie.com\n name: monitor_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _monitor_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: monitor_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - ai.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n name: ai_unified_service\n virtualHosts:\n - domains:\n - ai.jerxie.com\n - ai.jerxie.com:443\n name: ai_service\n routes:\n - match:\n prefix: /agent.\n route:\n autoHostRewrite: false\n cluster: _ai_agent_orchestrator\n maxStreamDuration:\n grpcTimeoutHeaderMax: 0s\n timeout: 0s\n - match:\n prefix: /\n route:\n cluster: _ai_unified_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n alpnProtocols:\n - h2\n - http/1.1\n tlsCertificateSdsSecretConfigs:\n - name: ai_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - container.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - container.jerxie.com\n name: container_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _portainer_ui\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: container_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - password.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - password.jerxie.com\n name: password_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _bitwarden_service\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: password_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - docker.jerxie.com\n - docker.local\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - docker.jerxie.com\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _docker_registry\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: docker_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - video.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - video.jerxie.com\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_video\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: video_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - audio.jerxie.com\n - audio.local\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - audio.jerxie.com\n - audio.local\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_audio\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: audio_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - gitbucket.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - gitbucket.jerxie.com\n name: gitbucket_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _git_bucket\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: gitbucket_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - photo.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - photo.jerxie.com\n name: photo_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_photo\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: photo_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - note.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - note.jerxie.com\n name: note_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_note\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: note_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - home.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n mergeSlashes: true\n normalizePath: true\n requestTimeout: 300s\n routeConfig:\n virtualHosts:\n - domains:\n - home.jerxie.com\n name: home_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _homeassistant_service\n statPrefix: ingress_http\n streamIdleTimeout: 300s\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: home_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - auth.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - auth.jerxie.com\n name: auth_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _auth_server\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: auth_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - nas\n - nas.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n maxRequestHeadersKb: 96\n routeConfig:\n virtualHosts:\n - domains:\n - nas.jerxie.com\n - nas:10001\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_service\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: nas_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - code.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2\n name: oidc_oauth2_config_code-server\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication\n name: oidc_jwt_authn_config\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua\n name: oidc_authz_lua\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - code.jerxie.com\n name: code_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _code_server\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n name: code_server_filter_chain\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: code_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - envoy.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2\n name: oidc_oauth2_config_envoy-server\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication\n name: oidc_jwt_authn_config\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua\n name: oidc_authz_lua\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - envoy.jerxie.com\n name: envoy_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _envoy_server\n statPrefix: ingress_http_envoy\n upgradeConfigs:\n - upgradeType: websocket\n name: envoy_server_filter_chain\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: envoy_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\nlistenerFilters:\n - name: envoy.filters.listener.tls_inspector\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector\nname: https_listener\n", "upsert": true}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9adf07f..cc54a13 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -22,6 +22,8 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-scripts": "5.0.1",
+ "reactflow": "^11.11.4",
+ "recharts": "^3.8.0",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -3396,6 +3398,136 @@
}
}
},
+ "node_modules/@reactflow/background": {
+ "version": "11.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+ "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/controls": {
+ "version": "11.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+ "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/core": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+ "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+ "dependencies": {
+ "@types/d3": "^7.4.0",
+ "@types/d3-drag": "^3.0.1",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/minimap": {
+ "version": "11.7.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+ "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-resizer": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+ "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.4",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-toolbar": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+ "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
+ "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3511,6 +3643,16 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
+ },
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -4291,6 +4433,228 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4369,6 +4733,11 @@
"@types/send": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -4596,6 +4965,11 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -6397,6 +6771,11 @@
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
"license": "MIT"
},
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
+ },
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -6429,6 +6808,14 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -7174,6 +7561,177 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -7268,6 +7826,11 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -7979,6 +8542,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.45.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
+ "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -10224,6 +10792,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -15835,6 +16411,28 @@
"react": ">=18"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -15966,6 +16564,23 @@
"node": ">=14.0.0"
}
},
+ "node_modules/reactflow": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+ "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+ "dependencies": {
+ "@reactflow/background": "11.3.14",
+ "@reactflow/controls": "11.2.14",
+ "@reactflow/core": "11.11.4",
+ "@reactflow/minimap": "11.7.14",
+ "@reactflow/node-resizer": "2.2.14",
+ "@reactflow/node-toolbar": "1.3.14"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -16001,6 +16616,46 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recharts": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
+ "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts/node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="
+ },
+ "node_modules/recharts/node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -16026,6 +16681,19 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -16222,6 +16890,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -18153,6 +18826,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"license": "MIT"
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -18706,6 +19384,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -18806,6 +19492,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@@ -19746,6 +20453,33 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 154e417..fdc4a42 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,8 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-scripts": "5.0.1",
+ "reactflow": "^11.11.4",
+ "recharts": "^3.8.0",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/frontend/src/App.js b/frontend/src/App.js
index b02cf78..be947d0 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -9,6 +9,7 @@
import { ProfilePage } from "./features/profile";
import { NodesPage } from "./features/nodes";
import { SkillsPage } from "./features/skills";
+import { AgentHarnessPage, AgentDrillDown } from "./features/agents";
import { getUserStatus, logout, getUserProfile } from "./services/apiService";
const Icon = ({ path, onClick, className }) => (
@@ -34,7 +35,7 @@
const [userId, setUserId] = useState(null);
const [userProfile, setUserProfile] = useState(null);
- const authenticatedPages = ["voice-chat", "swarm-control", "settings", "profile", "nodes", "skills"];
+ const authenticatedPages = ["voice-chat", "swarm-control", "settings", "profile", "nodes", "skills", "agents-harness", "agents-drilldown"];
const pageToPath = {
"home": "/",
"voice-chat": "/voice",
@@ -43,23 +44,29 @@
"profile": "/profile",
"nodes": "/nodes",
"skills": "/skills",
- "login": "/login"
+ "login": "/login",
+ "agents-harness": "/agents"
};
- const pathToPage = Object.fromEntries(Object.entries(pageToPath).map(([pk, pv]) => [pv, pk]));
+
+ const getPageFromPath = (path) => {
+ if (path === "/agents") return "agents-harness";
+ if (path.startsWith("/agents/drilldown/")) return "agents-drilldown";
+ const pathToPage = Object.fromEntries(Object.entries(pageToPath).map(([pk, pv]) => [pv, pk]));
+ return pathToPage[path] || "home";
+ };
// Sync state with URL on mount and handle popstate
useEffect(() => {
const handlePopState = () => {
const path = window.location.pathname;
- const page = pathToPage[path] || "home";
- setCurrentPage(page);
+ setCurrentPage(getPageFromPath(path));
};
window.addEventListener("popstate", handlePopState);
// Initial sync
const initialPath = window.location.pathname;
- const initialPage = pathToPage[initialPath] || "home";
+ const initialPage = getPageFromPath(initialPath);
if (initialPage !== currentPage) {
setCurrentPage(initialPage);
}
@@ -141,13 +148,22 @@
}
};
- const handleNavigate = (page) => {
- if (authenticatedPages.includes(page) && !isLoggedIn) {
+ const handleNavigate = (pageOrPath) => {
+ let targetPage = pageOrPath;
+ let targetPath = pageToPath[pageOrPath];
+
+ // If it's a raw path like /agents/drilldown/123
+ if (pageOrPath.startsWith('/')) {
+ targetPath = pageOrPath;
+ targetPage = getPageFromPath(pageOrPath);
+ }
+
+ if (authenticatedPages.includes(targetPage) && !isLoggedIn) {
setCurrentPage("login");
window.history.pushState({}, "", pageToPath["login"]);
} else {
- setCurrentPage(page);
- window.history.pushState({}, "", pageToPath[page] || "/");
+ setCurrentPage(targetPage);
+ window.history.pushState({}, "", targetPath || "/");
}
};
@@ -176,6 +192,11 @@
return
Changes to the system prompt will be immediately picked up by the agent loop on its next invocation turn.
+{modalConfig.message}
+Cortex System Orchestrator
+Configure a new autonomous instance to connect to the mesh.
+{result.message}
+ID: {result.instance_id}
+Are you sure you want to delete this agent? This cannot be undone.
+ {deleteError && ( +{deleteError}
+ )} +