327 lines
12 KiB
HTML
327 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Stellar Air — NOVA Platform Architecture</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
background: #0a0e17;
|
||
color: #e8eaf0;
|
||
font-family: 'Inter', sans-serif;
|
||
line-height: 1.6;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
header {
|
||
padding: 16px 24px;
|
||
border-bottom: 1px solid #1e2a4a;
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
header h1 {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
letter-spacing: 3px;
|
||
color: #0066ff;
|
||
}
|
||
|
||
header .subtitle {
|
||
font-size: 13px;
|
||
color: #4a5568;
|
||
letter-spacing: 1px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.layout {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
nav {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
width: 200px;
|
||
flex-shrink: 0;
|
||
background: #121829;
|
||
border-right: 1px solid #1e2a4a;
|
||
padding: 8px 0;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
nav a {
|
||
padding: 10px 20px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 12px;
|
||
color: #8892a8;
|
||
text-decoration: none;
|
||
border-left: 2px solid transparent;
|
||
transition: all 0.15s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
nav a:hover { color: #e8eaf0; background: #1a2340; }
|
||
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
|
||
|
||
main {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 32px 48px;
|
||
}
|
||
|
||
.graph-section {
|
||
display: none;
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
.graph-section.active { display: block; }
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
.graph-section h2 {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #8892a8;
|
||
margin-bottom: 8px;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.graph-section p {
|
||
font-size: 13px;
|
||
color: #4a5568;
|
||
margin-bottom: 24px;
|
||
max-width: 800px;
|
||
}
|
||
|
||
.graph-container {
|
||
background: #0a0e17;
|
||
border: 1px solid #1e2a4a;
|
||
padding: 24px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.graph-container img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
.legend {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-top: 16px;
|
||
font-size: 11px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.legend span::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
margin-right: 6px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.legend .live::before { background: #00c853; }
|
||
.legend .mock::before { background: #ffc107; }
|
||
.legend .mcp::before { background: #0066ff; }
|
||
.legend .ops::before { background: #ff3d00; }
|
||
|
||
.graph-container a { display: block; }
|
||
.graph-container img { max-width: 100%; height: auto; }
|
||
|
||
/* Repo tree */
|
||
.tree-container {
|
||
background: #0a0e17;
|
||
border: 1px solid #1e2a4a;
|
||
padding: 24px;
|
||
overflow: auto;
|
||
}
|
||
.repo-tree {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
color: #8892a8;
|
||
}
|
||
.t-root { color: #0066ff; font-weight: 600; font-size: 15px; }
|
||
.t-dir { color: #e8eaf0; font-weight: 500; }
|
||
.t-mcp { color: #0066ff; font-weight: 500; }
|
||
.t-ops { color: #ff3d00; font-weight: 500; }
|
||
.t-pax { color: #00c853; font-weight: 500; }
|
||
.t-live { color: #00c853; }
|
||
.t-comment { color: #4a5568; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<h1>STELLAR AIR</h1>
|
||
<span class="subtitle">NOVA Operations Platform — Architecture</span>
|
||
</header>
|
||
|
||
<div class="layout">
|
||
|
||
<nav>
|
||
<a class="active" onclick="show('system')">System</a>
|
||
<a onclick="show('mcp')">MCP Servers</a>
|
||
<a onclick="show('efhas')">FCE Agent</a>
|
||
<a onclick="show('handover')">Handover Agent</a>
|
||
<a onclick="show('data')">Data Flow</a>
|
||
<a onclick="show('deploy')">Deployment</a>
|
||
<a onclick="show('repo')">Repository</a>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<section id="system" class="graph-section active">
|
||
<h2>SYSTEM ARCHITECTURE</h2>
|
||
<p>End-to-end view: Vue UI → Kong gateway (optional) → FastAPI → MCP servers → live and scenario data sources. Langfuse (separate shared cluster) traces every agent run and tool call.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/system_architecture.svg"><img src="graphs/system_architecture.svg" alt="System Architecture"></a>
|
||
</div>
|
||
<div class="legend">
|
||
<span class="live">Live API</span>
|
||
<span class="mock">Scenario data</span>
|
||
<span class="mcp">MCP protocol</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="mcp" class="graph-section">
|
||
<h2>MCP SERVER TOPOLOGY</h2>
|
||
<p>Three servers scoped by access domain. Each exposes tools, resources, and prompts. FCE connects to shared + passenger. Handover connects to shared + ops.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/mcp_servers.svg"><img src="graphs/mcp_servers.svg" alt="MCP Servers"></a>
|
||
</div>
|
||
<div class="legend">
|
||
<span class="mcp">Shared server</span>
|
||
<span class="ops">Ops server</span>
|
||
<span class="live">Passenger server</span>
|
||
</div>
|
||
<div class="legend" style="margin-top: 8px;">
|
||
<span style="color:#8892a8">── solid = tool calls</span>
|
||
<span style="color:#8892a8">╌╌ dashed = resource reads</span>
|
||
<span style="color:#8892a8">··· dotted = prompt gets</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="efhas" class="graph-section">
|
||
<h2>FCE AGENT — BEHIND EVERY DEPARTURE</h2>
|
||
<p>Passenger notification agent. Triages flight status, gathers context from 5 parallel tool calls (including live weather and FAA data), synthesizes an empathetic notification.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/efhas_agent.svg"><img src="graphs/efhas_agent.svg" alt="FCE Agent"></a>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="handover" class="graph-section">
|
||
<h2>SHIFT HANDOVER AGENT</h2>
|
||
<p>Ops briefing agent. Scans all hubs in parallel, scores issues by severity × time sensitivity, categorizes into IMMEDIATE / MONITOR / FYI, generates a structured brief.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/handover_agent.svg"><img src="graphs/handover_agent.svg" alt="Handover Agent"></a>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="data" class="graph-section">
|
||
<h2>DATA FLOW — REAL vs MOCK</h2>
|
||
<p>Weather and FAA airport status are live (no API key). Flight, crew, passenger, and maintenance data are scenario-based fixtures switchable from the UI.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/data_flow.svg"><img src="graphs/data_flow.svg" alt="Data Flow"></a>
|
||
</div>
|
||
<div class="legend">
|
||
<span class="live">Live data (no API key)</span>
|
||
<span class="mock">Scenario data (switchable)</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="deploy" class="graph-section">
|
||
<h2>DEPLOYMENT</h2>
|
||
<p>Kind cluster for dev (Tilt), docker-compose for EC2 production (nova-api + nova-ui on shared gateway network). Woodpecker CI builds images on push to main. EC2 nginx proxies stellarair.mcrn.ar → container; Kong Konnect available as optional governance layer.</p>
|
||
<div class="graph-container">
|
||
<a href="viewer.html?src=graphs/deployment.svg"><img src="graphs/deployment.svg" alt="Deployment"></a>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="repo" class="graph-section">
|
||
<h2>REPOSITORY STRUCTURE</h2>
|
||
<p>Monorepo: MCP servers, agents, IRROP engine, API, Vue UI (with shared component framework), and deployment configs.</p>
|
||
<div class="tree-container">
|
||
<pre class="repo-tree"><span class="t-root">stellar-ops/</span>
|
||
├── <span class="t-dir">mcp_servers/</span>
|
||
│ ├── <span class="t-mcp">shared/</span> <span class="t-comment">server.py · tools.py · resources.py · prompts.py</span>
|
||
│ │ └── tools: <span class="t-live">get_route_weather</span> · <span class="t-live">get_hub_forecasts</span> · <span class="t-live">get_airport_status</span>
|
||
│ │ get_flight_status · get_flight_details · get_irregular_ops
|
||
│ │ get_airport_congestion · get_maintenance_flags
|
||
│ ├── <span class="t-ops">ops/</span> <span class="t-comment">server.py · tools.py · resources.py · prompts.py</span>
|
||
│ │ └── tools: get_crew_notes · get_crew_duty_status · get_pending_rebookings
|
||
│ │ generate_narrative
|
||
│ ├── <span class="t-pax">passenger/</span> <span class="t-comment">server.py · tools.py · resources.py · prompts.py</span>
|
||
│ │ └── tools: generate_notification
|
||
│ ├── shared_llm.py <span class="t-comment">multi-provider: Groq · Anthropic · Bedrock · OpenAI</span>
|
||
│ └── <span class="t-dir">data/</span>
|
||
│ ├── models.py <span class="t-comment">FlightData · CrewMember · Passenger · MELItem · HubInfo</span>
|
||
│ ├── real/ <span class="t-live">openmeteo.py · faa.py</span>
|
||
│ └── scenarios/ <span class="t-comment">normal_ops · weather_disruption_ord</span>
|
||
│ <span class="t-comment">maintenance_delay_sfo · crew_swap_ewr</span>
|
||
├── <span class="t-dir">agents/</span>
|
||
│ ├── fce.py <span class="t-comment">FCE — "Behind Every Departure" (passenger notifications)</span>
|
||
│ ├── handover.py <span class="t-comment">Shift Handover (ops brief: IMMEDIATE / MONITOR / FYI)</span>
|
||
│ └── shared/
|
||
│ ├── mcp_client.py <span class="t-comment">MCPMultiClient + connect_servers context manager</span>
|
||
│ ├── parser.py <span class="t-comment">parse_tool_result · parse_resource_result · parse_prompt_result</span>
|
||
│ └── tool_runner.py <span class="t-comment">build_tool_caller — timeout · Langfuse span · error collection</span>
|
||
├── <span class="t-dir">api/</span>
|
||
│ ├── main.py <span class="t-comment">FastAPI: agents, scenarios, WebSocket, /health, Langfuse traces</span>
|
||
│ └── config.py <span class="t-comment">Pydantic Settings — centralized env var reads</span>
|
||
├── <span class="t-dir">ui/</span>
|
||
│ ├── framework/ <span class="t-comment">soleprint-ui (shared component library)</span>
|
||
│ └── app/ <span class="t-comment">Vue 3 SPA — Operations · Internals · Data · Settings</span>
|
||
│ └── src/config.ts <span class="t-comment">Kong proxy URL + API/WS base</span>
|
||
├── <span class="t-dir">ctrl/</span>
|
||
│ ├── Dockerfile.api/ui <span class="t-comment">Container builds</span>
|
||
│ ├── nginx.conf <span class="t-comment">UI nginx (proxies /agents /scenarios /config /health /ws)</span>
|
||
│ ├── k8s/ <span class="t-comment">base/ + overlays/dev/ (Kustomize)</span>
|
||
│ ├── Tiltfile <span class="t-comment">Dev environment (Kind cluster: unt)</span>
|
||
│ ├── edge/ <span class="t-comment">Production docker-compose (nova-api + nova-ui on gateway net)</span>
|
||
│ └── deploy.sh <span class="t-comment">rsync (bypass CI) · edge (pull registry images)</span>
|
||
├── <span class="t-dir">tests/</span> <span class="t-comment">69 tests: models · clients · MCP · scenarios · agents</span>
|
||
│ └── base.py <span class="t-comment">dual-mode: inprocess (default) · live (CONTRACT_TEST_MODE=live)</span>
|
||
├── <span class="t-dir">.woodpecker/</span> <span class="t-comment">CI pipeline — build API + UI, push to registry.mcrn.ar</span>
|
||
├── <span class="t-dir">docs/</span> <span class="t-comment">Architecture graphs (this page)</span>
|
||
└── .mcp.json <span class="t-comment">Claude Code integration — 3 servers</span></pre>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
function show(id) {
|
||
document.querySelectorAll('.graph-section').forEach(s => s.classList.remove('active'));
|
||
document.querySelectorAll('nav a').forEach(a => a.classList.remove('active'));
|
||
document.getElementById(id).classList.add('active');
|
||
event.currentTarget.classList.add('active');
|
||
}
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|